Введите стирание, введите стирание, есть вопросы?

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

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

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
}

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


person Yakk - Adam Nevraumont    schedule 08.08.2016    source источник
comment
Тангенциально связанные; с какой реализацией вы это тестируете? Есть ли у какой-либо из основных стандартных библиотек готовые версии?   -  person TartanLlama    schedule 08.08.2016
comment
Я чувствую, что когда я пытался делать подобные вещи в прошлом, я в конце концов понял, что по сути все возвращалось к шаблонным виртуальным машинам и к тому факту, что они не разрешены языком. Я уверен, что что-то возможно, но, безусловно, многие из лучших решений невозможны по этой причине.   -  person Nir Friedman    schedule 08.08.2016
comment
@TartanLlama Тестируете? Вы имеете в виду это, или вариант один? Я этого не сделал. Только что делал против boost::variant и нашел опечатку. По большей части, за исключением небольших различий и уродства синтаксиса, которые C++ 17 убирает, тестирования std::any решений против boost::any будет достаточно, чтобы быть уверенным, по крайней мере, на онлайн-компиляторе.   -  person Yakk - Adam Nevraumont    schedule 08.08.2016
comment
@NirFriedman Вы говорите о том, чтобы попробовать что-то подобное, когда вы хотите иметь возможность (скажем) печатать что угодно вообще и в конечном итоге использовать введите стирание, чтобы сделать это... или вы говорите о стирании типа стирания типа самого и затем сталкиваетесь с проблемами? Виртуальная нехватка шаблонов по большей части решается стиранием типов, если только вы не хотите выполнять множественную диспетчеризацию по двум неограниченным наборам типов аргументов в разных местах кода.   -  person Yakk - Adam Nevraumont    schedule 08.08.2016
comment
Я не уверен, что собрал все части воедино, но вот очень грубый набросок: coliru.stacked-crooked.com/a/2ab8d7e41d24e616   -  person dyp    schedule 08.08.2016
comment
Слегка улучшена перегрузка операторов: coliru.stacked-crooked.com/a/23a25da83c5ba11d   -  person dyp    schedule 08.08.2016
comment
@dyp да, это доказательство концепции. Не отделяет хранилище указателя функции от фабрики указателя функции (фабрика должна быть глобальной, хранилище указателя функции локально для any), но это просто проблема дизайна.   -  person Yakk - Adam Nevraumont    schedule 09.08.2016
comment
@dyp Итеративное улучшение вашего кода, объединенного с моим (в настоящее время удаленным) ответом ниже.   -  person Yakk - Adam Nevraumont    schedule 09.08.2016
comment
@Yakk Если бы существовали виртуальные шаблоны, то any могла бы просто иметь шаблонную виртуальную функцию с именем apply, которая принимала вариативный функтор, а в производном any (который знает тип) реализация apply вызывала бы функтор для производного типа . В этом смысле виртуальные шаблоны тривиально решат вашу проблему. С вариантами проблема легко решаема, потому что они используют регистр переключения, а не виртуальные, в качестве косвенности во время выполнения; это ключевое отличие.   -  person Nir Friedman    schedule 09.08.2016
comment
Когда я прочитал пример документации с вариантом и оператором-›*, я понял, что это вы, даже не глядя на имя   -  person Johannes Schaub - litb    schedule 09.08.2016
comment
Если вам разрешено использовать Boost, вы можете попробовать Boost.TypeErasure (пример использования)   -  person milleniumbug    schedule 10.08.2016
comment
@milleniumbug Я не знаю, как получить гладкий ->*dofoo или аналогичный синтаксис с Boost.TypeErasure, но я не знаком с ним.   -  person Yakk - Adam Nevraumont    schedule 10.08.2016
comment
Не могли бы вы обновить вопрос, чтобы избежать ссылок на SO Docs? Пожалуйста, отметьте этот комментарий как «больше не нужен», когда закончите. (См. раздел Удаление документации: репутация, архив и ссылки. если вы не в курсе, что происходит и что делать.) Спасибо.   -  person Jonathan Leffler    schedule 22.09.2017


Ответы (2)


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

любой_метод:

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

супер_любой:

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
}

Текущий

person cpplearner    schedule 10.08.2016
comment
Slick, используя сигнатуру указателя функции, включая тип any_method, но принципиально ограничивает вас уникальным типом any_method (по крайней мере, для одного и того же объекта). Я не вижу способа легко перейти на аргументы указателя auto* в super_any<&print>. Я предполагаю, что это взлом с использованием тега auto*. Я предполагаю, что это крайний случай, так как нам нужен полиморфизм времени компиляции в any_method, а auto&& self в лямбде — самый простой способ сделать это. Гораздо меньше моего! (+1) - person Yakk - Adam Nevraumont; 10.08.2016

Это решение использует 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, который мы создаем глобально и 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)...);
  }
};

Фабричный метод, который не нужен в С++ 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
comment
Я думаю, что должна быть возможность использовать виртуальные функции вместо указателей на функции, создав новый тип, производный от экземпляров шаблона класса, которые создают необходимый код в super_any_t::set_operation_to. При множественном наследовании это должно быть примерно таким же коротким, как присваивание указателям на функции. Поскольку вы ограничиваете входные функции, чтобы они были без состояния/чистыми, хранение этих указателей в виртуальной таблице один раз для каждого списка типов входных функций представляется возможным. - person dyp; 09.08.2016
comment
так как у меня нет компилятора С++ 17. Вы имеете в виду помимо Wandbox и любого другого онлайн-компилятора? Также: apt.llvm.org. - person TemplateRex; 09.08.2016
comment
@TemplateRex Я думаю, что Yakk имел в виду, что ни один из компиляторов не поддерживает полностью С++ 17 здесь параметры автоматического шаблона ... - person W.F.; 09.08.2016
comment
@dyp ручная vtable, где мы создаем указатель на статический tuple, созданный для каждого типа, будет лучше. Накладные расходы на один указатель на экземпляр super any вместо одного на метод на экземпляр. Это позволяет избежать использования new (будь то размещение или нет). Размещение new бесполезно, так как для получения правильного размера требуется удача (или статические утверждения, или небезумные компиляторы), а также добавляется уровень косвенности, если не быть очень осторожным. - person Yakk - Adam Nevraumont; 09.08.2016
comment
Можно ли избавиться от подписи в any_method_function ? Так можно ли использовать параметры шаблона? Как std::variant с std::visit. - person tower120; 11.06.2018
comment
@tower, что нам проблема двойной отправки. Исследуйте решения без лишнего стирания; аналогичный можно было бы сделать здесь. Но это ортогональные задачи. - person Yakk - Adam Nevraumont; 11.06.2018
comment
@ Tower120 Tower120 Вы решили свою проблему без использования стирания типа super_any, используя обычные виртуальные функции? Если нет, то я даже не знаю, как с вами поговорить о решении в этой более сложной области. Я не могу сказать из вашего ответа. Но решение для двойной отправки в случае виртуального наследования и решение здесь будут в основном одинаковыми. Ваше предполагаемое решение не похоже на виртуальный случай двойной отправки, отсюда и мое замешательство. - person Yakk - Adam Nevraumont; 11.06.2018
comment
@Yakk-AdamNevraumont Чтобы решить эту проблему с помощью виртуальных функций, вам нужны шаблонные виртуальные функции ... задуманное не является решением - это то, о чем я спрашиваю; вот немного измененная версия pastebin.com/hXXPLNFP . Единственное отличие от вашего РАБОЧЕГО решения - make_any_method не имеет сигнатуры функции. Сначала я спросил, возможно ли это? Вы сказали, что есть способ сделать это. Я этого не вижу. - person tower120; 11.06.2018
comment
@ башня120 Хорошо. Так что успокойтесь и гуглите двойную отправку. Двойная диспетчеризация — это способ виртуальной диспетчеризации одной функции на основе динамического типа двух ее аргументов (скажем, указателя this и аргумента функции). Существует целый ряд работ по двойной диспетчеризации в C++, включая статью Бьярна. Существует целый ряд решений, начиная от ручных посетителей и заканчивая использованием вариантов и автоматическим написанием методов с помощью CRTP вплоть до статьи Бьярна. Эта задача полностью ортогональна super_any. - person Yakk - Adam Nevraumont; 11.06.2018
comment
@ Tower120 Так что это возможно, но это не то, о чем вам следует говорить в ветке комментариев в несвязанном вопросе о переполнении стека. Я продолжал говорить о проблеме двойной отправки. Пока вы не осознаете эту проблему, у меня даже нет словарного запаса, чтобы поговорить с вами об этом. И нет, я не собираюсь решать обе проблемы одновременно в ветке комментариев к SO, но я утверждаю, что это решаемо в зависимости от точных деталей проблемы, о которой я снова не могу даже говорить, пока вы действительно не сделаете домашнюю работу. - person Yakk - Adam Nevraumont; 11.06.2018