Тип стирающий, тип стирающий, `любые` вопросы?

Итак, предположим, что я хочу набрать стирание, используя стирание типа.

Я могу создать псевдо-методы для вариантов, которые включают естественный:

pseudo_method print = [](auto&& self, auto&& os){ os << self; };

std::variant<A,B,C> var = // create a variant of type A B or C

(var->*print)(std::cout); // print it out without knowing what it is

Мой вопрос, как мне расширить это до std::any?

Это не может быть сделано «в сыром». Но в тот момент, когда мы назначаем / построить std::any у нас есть информация о типе, нам нужно.

Таким образом, в теории, дополненная any:

template<class...OperationsToTypeErase>
struct super_any {
std::any data;
// or some transformation of OperationsToTypeErase?
std::tuple<OperationsToTypeErase...> operations;
// ?? what for ctor/assign/etc?
};

может каким-то образом автоматически перепривязать некоторый код так, чтобы синтаксис вышеуказанного типа работал.

В идеале это было бы так же кратко в использовании, как вариант варианта.

template<class...Ops, class Op,
// SFINAE filter that an op matches:
std::enable_if_t< std::disjunction< std::is_same<Ops, Op>... >{}, int>* =nullptr
>
decltype(auto) operator->*( super_any<Ops...>& a, any_method<Op> ) {
return std::get<Op>(a.operations)(a.data);
}

Теперь я могу оставить это тип, все же разумно использовать лямбда-синтаксис для простоты?

В идеале я хочу:

any_method<void(std::ostream&)> print =
[](auto&& self, auto&& os){ os << self; };

using printable_any = make_super_any<&print>;

printable_any bob = 7; // sets up the printing data attached to the any

int main() {
(bob->*print)(std::cout); // prints 7
bob = 3.14159;
(bob->*print)(std::cout); // prints 3.14159
}

или похожий синтаксис. Это невозможно? Неосуществимым? Легко?

23

Решение

Это решение, которое использует 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);

что почти оптимально. С тем, что я считаю простыми изменениями в C ++ 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, Таким образом, мы объединяем указатели функций в vtable:

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 );
}
};

мы могли бы специализировать это для случаев, когда 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, что мы создаем глобально и constLY.

Мы храним объект, с которым мы строим это внутри 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)...);
}
};

Заводской метод, не необходимый в C ++ 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 кажется разумным, по крайней мере, на лязг

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_methods в кортеже 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 <<).

10

Другие решения

Вот мое решение. Он выглядит короче, чем у Якка, и не использует std::aligned_storage и размещение новых. Кроме того, он поддерживает сохраняющие состояние и локальные функторы (что подразумевает невозможность записи super_any<&print>, поскольку print может быть локальной переменной).

any_method:

template<class F, class Sig> struct any_method;

template<class F, class Ret, class... Args> struct any_method<F,Ret(Args...)> {
F f;
template<class T>
static Ret invoker(any_method& self, boost::any& data, Args... args) {
return self.f(boost::any_cast<T&>(data), std::forward<Args>(args)...);
}
using invoker_type = Ret (any_method&, boost::any&, Args...);
};

make_any_method:

template<class Sig, class F>
any_method<std::decay_t<F>,Sig> make_any_method(F&& f) {
return { std::forward<F>(f) };
}

super_any:

template<class...OperationsToTypeErase>
struct super_any {
boost::any data;
std::tuple<typename OperationsToTypeErase::invoker_type*...> operations = {};

template<class T, class ContainedType = std::decay_t<T>>
super_any(T&& t)
: data(std::forward<T>(t))
, operations((OperationsToTypeErase::template invoker<ContainedType>)...)
{}

template<class T, class ContainedType = std::decay_t<T>>
super_any& operator=(T&& t) {
data = std::forward<T>(t);
operations = { (OperationsToTypeErase::template invoker<ContainedType>)... };
return *this;
}
};

оператор -> *:

template<class...Ops, class F, class Sig,
// SFINAE filter that an op matches:
std::enable_if_t< std::disjunction< std::is_same<Ops, any_method<F,Sig>>... >{}, int> = 0
>
auto operator->*( super_any<Ops...>& a, any_method<F,Sig> f) {
auto fptr = std::get<typename any_method<F,Sig>::invoker_type*>(a.operations);
return [fptr,f, &a](auto&&... args) mutable {
return fptr(f, a.data, std::forward<decltype(args)>(args)...);
};
}

Использование:

#include <iostream>
auto print = make_any_method<void(std::ostream&)>(
[](auto&& self, auto&& os){ os << self; }
);

using printable_any = super_any<decltype(print)>;

printable_any bob = 7; // sets up the printing data attached to the any

int main() {
(bob->*print)(std::cout); // prints 7
bob = 3.14159;
(bob->*print)(std::cout); // prints 3.14159
}

Жить

7

По вопросам рекламы [email protected]