Это решение использует C++14 и boost::any
, так как у меня нет компилятора C++17.
Синтаксис, который мы получаем, следующий:
const auto print =
make_any_method<void(std::ostream&)>([](auto&& p, std::ostream& t){ t << p << "\n"; });
super_any<decltype(print)> a = 7;
(a->*print)(std::cout);
что почти оптимально. С тем, что я считаю простыми изменениями С++ 17, это должно выглядеть так:
constexpr any_method<void(std::ostream&)> print =
[](auto&& p, std::ostream& t){ t << p << "\n"; };
super_any<&print> a = 7;
(a->*print)(std::cout);
В C++17 я бы улучшил это, взяв auto*...
указателей на any_method
вместо шума decltype
.
Публично наследовать от any
немного рискованно, так как если кто-то возьмет any
сверху и изменит его, tuple
из any_method_data
будет устаревшим. Вероятно, нам следует просто имитировать весь интерфейс any
, а не публично наследовать его.
@dyp написал доказательство концепции в комментариях к ОП. Это основано на его работе, очищенной с добавлением семантики значений (украденной у boost::any
). Решение @cpplearner на основе указателей было использовано, чтобы сократить его (спасибо!), а затем я добавил оптимизацию vtable поверх этого.
Сначала мы используем тег для передачи типов:
template<class T>struct tag_t{constexpr tag_t(){};};
template<class T>constexpr tag_t<T> tag{};
Этот трейт-класс получает подпись, хранящуюся в any_method
:
Это создает тип указателя функции и фабрику для указанных указателей функций с учетом any_method
:
template<class any_method, class Sig=any_sig_from_method<any_method>>
struct any_method_function;
template<class any_method, class R, class...Args>
struct any_method_function<any_method, R(Args...)>
{
using type = R(*)(boost::any&, any_method const*, Args...);
template<class T>
type operator()( tag_t<T> )const{
return [](boost::any& self, any_method const* method, Args...args) {
return (*method)( boost::any_cast<T&>(self), decltype(args)(args)... );
};
}
};
Теперь мы не хотим хранить указатель на функцию для каждой операции в нашем файле super_any
. Итак, мы объединяем указатели функций в виртуальную таблицу:
template<class...any_methods>
using any_method_tuple = std::tuple< typename any_method_function<any_methods>::type... >;
template<class...any_methods, class T>
any_method_tuple<any_methods...> make_vtable( tag_t<T> ) {
return std::make_tuple(
any_method_function<any_methods>{}(tag<T>)...
);
}
template<class...methods>
struct any_methods {
private:
any_method_tuple<methods...> const* vtable = 0;
template<class T>
static any_method_tuple<methods...> const* get_vtable( tag_t<T> ) {
static const auto table = make_vtable<methods...>(tag<T>);
return &table;
}
public:
any_methods() = default;
template<class T>
any_methods( tag_t<T> ): vtable(get_vtable(tag<T>)) {}
any_methods& operator=(any_methods const&)=default;
template<class T>
void change_type( tag_t<T> ={} ) { vtable = get_vtable(tag<T>); }
template<class any_method>
auto get_invoker( tag_t<any_method> ={} ) const {
return std::get<typename any_method_function<any_method>::type>( *vtable );
}
};
мы могли бы специализировать это для случаев, когда виртуальная таблица небольшая (например, 1 элемент), и использовать прямые указатели, хранящиеся в классе, в этих случаях для эффективности.
Теперь начинаем super_any
. Я использую super_any_t
, чтобы упростить объявление super_any
.
template<class...methods>
struct super_any_t;
Это ищет методы, которые super any поддерживает для SFINAE:
template<class super_any, class method>
struct super_method_applies : std::false_type {};
template<class M0, class...Methods, class method>
struct super_method_applies<super_any_t<M0, Methods...>, method> :
std::integral_constant<bool, std::is_same<M0, method>{} || super_method_applies<super_any_t<Methods...>, method>{}>
{};
Это указатель на псевдометод, такой как print
, который мы создаем глобально и const
ly.
Мы сохраняем объект, который мы создаем, внутри файла any_method
. Обратите внимание, что если вы создадите его с помощью не-лямбда, все может стать неудобным, так как тип этого any_method
используется как часть механизма диспетчеризации.
template<class Sig, class F>
struct any_method {
using signature=Sig;
private:
F f;
public:
template<class Any,
// SFINAE testing that one of the Anys's matches this type:
std::enable_if_t< super_method_applies< std::decay_t<Any>, any_method >{}, int>* =nullptr
>
friend auto operator->*( Any&& self, any_method const& m ) {
// we don't use the value of the any_method, because each any_method has
// a unique type (!) and we check that one of the auto*'s in the super_any
// already has a pointer to us. We then dispatch to the corresponding
// any_method_data...
return [&self, invoke = self.get_invoker(tag<any_method>), m](auto&&...args)->decltype(auto)
{
return invoke( decltype(self)(self), &m, decltype(args)(args)... );
};
}
any_method( F fin ):f(std::move(fin)) {}
template<class...Args>
decltype(auto) operator()(Args&&...args)const {
return f(std::forward<Args>(args)...);
}
};
Фабричный метод, который не нужен в С++ 17, я считаю:
template<class Sig, class F>
any_method<Sig, std::decay_t<F>>
make_any_method( F&& f ) {
return {std::forward<F>(f)};
}
Это расширенный any
. Это и any
, и он несет в себе набор указателей на функции стирания типов, которые изменяются всякий раз, когда это делает any
:
template<class... methods>
struct super_any_t:boost::any, any_methods<methods...> {
private:
template<class T>
T* get() { return boost::any_cast<T*>(this); }
public:
template<class T,
std::enable_if_t< !std::is_same<std::decay_t<T>, super_any_t>{}, int>* =nullptr
>
super_any_t( T&& t ):
boost::any( std::forward<T>(t) )
{
using dT=std::decay_t<T>;
this->change_type( tag<dT> );
}
super_any_t()=default;
super_any_t(super_any_t&&)=default;
super_any_t(super_any_t const&)=default;
super_any_t& operator=(super_any_t&&)=default;
super_any_t& operator=(super_any_t const&)=default;
template<class T,
std::enable_if_t< !std::is_same<std::decay_t<T>, super_any_t>{}, int>* =nullptr
>
super_any_t& operator=( T&& t ) {
((boost::any&)*this) = std::forward<T>(t);
using dT=std::decay_t<T>;
this->change_type( tag<dT> );
return *this;
}
};
Поскольку мы храним any_method
как объекты const
, это немного упрощает создание super_any
:
template<class...Ts>
using super_any = super_any_t< std::remove_const_t<std::remove_reference_t<Ts>>... >;
Тестовый код:
const auto print = make_any_method<void(std::ostream&)>([](auto&& p, std::ostream& t){ t << p << "\n"; });
const auto wprint = make_any_method<void(std::wostream&)>([](auto&& p, std::wostream& os ){ os << p << L"\n"; });
const auto wont_work = make_any_method<void(std::ostream&)>([](auto&& p, std::ostream& t){ t << p << "\n"; });
struct X {};
int main()
{
super_any<decltype(print), decltype(wprint)> a = 7;
super_any<decltype(print), decltype(wprint)> a2 = 7;
(a->*print)(std::cout);
(a->*wprint)(std::wcout);
// (a->*wont_work)(std::cout);
double d = 4.2;
a = d;
(a->*print)(std::cout);
(a->*wprint)(std::wcout);
(a2->*print)(std::cout);
(a2->*wprint)(std::wcout);
// a = X{}; // generates an error if you try to store a non-printable
}
живой пример.
Сообщение об ошибке, когда я пытаюсь сохранить непечатаемое struct X{};
внутри super_any
, кажется разумным, по крайней мере, для clang:
main.cpp:150:87: error: invalid operands to binary expression ('std::ostream' (aka 'basic_ostream<char>') and 'X')
const auto x0 = make_any_method<void(std::ostream&)>([](auto&& p, std::ostream& t){ t << p << "\n"; });
это происходит в тот момент, когда вы пытаетесь присвоить X{}
super_any<decltype(x0)>
.
Структура any_method
достаточно совместима с pseudo_method
, который действует аналогично на варианты, поэтому их, вероятно, можно объединить.
Я использовал ручную vtable здесь, чтобы сохранить накладные расходы на стирание типа до 1 указателя на super_any
. Это добавляет стоимость перенаправления к каждому вызову any_method. Мы могли бы очень легко хранить указатели непосредственно в super_any
, и было бы несложно сделать это параметром для super_any
. В любом случае, в случае с 1 стертым методом, мы должны просто сохранить его напрямую.
Два разных any_method
одного и того же типа (скажем, оба содержат указатель на функцию) порождают одинаковый тип super_any
. Это вызывает проблемы при поиске.
Различить их немного сложно. Если бы мы изменили super_any
на auto* any_method
, мы могли бы объединить все any_method
одинакового типа в кортеж vtable, а затем выполнить линейный поиск соответствующего указателя, если их больше 1. Линейный поиск должен быть оптимизирован с помощью компилятор, если вы не делаете что-то сумасшедшее, например, передаете ссылку или указатель на то, какой конкретный any_method
мы используем.
Однако это выходит за рамки этого ответа; существования этого улучшения пока достаточно.
Кроме того, можно добавить ->*
, который принимает указатель (или даже ссылку!) с левой стороны, что позволяет обнаруживать это и также передавать это лямбда-выражению. Это может сделать его действительно «любым методом», поскольку он работает с вариантами, super_anys и указателями с этим методом.
Немного поработав if constexpr
, лямбда может разветвляться при выполнении ADL или вызове метода в любом случае.
Это должно дать нам:
(7->*print)(std::cout);
((super_any<&print>)(7)->*print)(std::cout); // C++17 version of above syntax
((std::variant<int, double>{7})->*print)(std::cout);
int* ptr = new int(7);
(ptr->*print)(std::cout);
(std::make_unique<int>(7)->*print)(std::cout);
(std::make_shared<int>(7)->*print)(std::cout);
с any_method
просто «делает правильно» (то есть передает значение std::cout <<
).
person
Yakk - Adam Nevraumont
schedule
08.08.2016
boost::variant
и нашел опечатку. По большей части, за исключением небольших различий и уродства синтаксиса, которые C++ 17 убирает, тестированияstd::any
решений противboost::any
будет достаточно, чтобы быть уверенным, по крайней мере, на онлайн-компиляторе. - person Yakk - Adam Nevraumont   schedule 08.08.2016any
), но это просто проблема дизайна. - person Yakk - Adam Nevraumont   schedule 09.08.2016any
могла бы просто иметь шаблонную виртуальную функцию с именемapply
, которая принимала вариативный функтор, а в производномany
(который знает тип) реализацияapply
вызывала бы функтор для производного типа . В этом смысле виртуальные шаблоны тривиально решат вашу проблему. С вариантами проблема легко решаема, потому что они используют регистр переключения, а не виртуальные, в качестве косвенности во время выполнения; это ключевое отличие. - person Nir Friedman   schedule 09.08.2016->*dofoo
или аналогичный синтаксис с Boost.TypeErasure, но я не знаком с ним. - person Yakk - Adam Nevraumont   schedule 10.08.2016