键入擦除类型擦除,“任何”问题?

时间:2016-08-08 18:04:46

标签: c++ type-erasure c++17 stdany

所以,假设我想使用类型擦除键入erase。

我可以为支持自然的变体创建伪方法:

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

现在我可以将它保存到类型,但是合理地使用lambda语法来保持简单吗?

理想情况下我想:

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
}

或类似的语法。这不可能吗?不可行?易?

2 个答案:

答案 0 :(得分:10)

这是一个使用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的{​​{1}}将过期。可能我们应该模仿整个any_method_data界面,而不是公开继承。

@dyp在OP的评论中写了一个概念证明。这是基于他的工作,清理了价值语义(从any被盗)。 @cpplearner的基于指针的解决方案用于缩短它(谢谢!),然后我在其上添加了vtable优化。

首先我们使用标签传递类型:

boost::any

此特征类获取与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)... ); }; } }; 中为每个操作存储一个函数指针。所以我们将函数指针捆绑成一个vtable:

super_any

我们可以专门针对vtable很小的情况(例如,1项),并在这些情况下使用存储在类中的直接指针来提高效率。

现在我们开始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 ); } }; 。我使用super_any使super_any_t的声明更容易一些。

super_any

这将搜索super any支持SFINAE的方法:

template<class...methods>
struct super_any_t;

这是伪方法指针,如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中。请注意,如果使用非lambda构造它,则可能会变得毛茸茸,因为此any_method type 被用作调度机制的一部分。

any_method

C ++中不需要的工厂方法17我相信:

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

这是增强的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>>... >;

live example

当我尝试在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{};时,错误消息似乎至少在clang上是合理的:

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将类型擦除开销保持为每super_any 1个指针。这会为每个any_method调用添加重定向成本。我们可以非常轻松地将指针直接存储在super_any中,并且将参数设置为super_any并不困难。在任何情况下,在1擦除方法的情况下,我们应该直接存储它。

相同类型的两个不同的any_method(例如,都包含函数指针)产生相同类型的super_any。这会导致查找问题。

区分它们有点棘手。如果我们将super_any更改为auto* any_method,我们可以将所有相同类型的any_method捆绑在vtable元组中,然后对匹配的指针执行线性搜索(如果有)线性搜索应该由编译器优化,除非你做了一些疯狂的事情,比如传递一个引用或指向我们正在使用的特定any_method的指针。

然而,这似乎超出了这个答案的范围;现在已经足够存在这种改进了。

此外,可以添加在左侧获取指针(或甚至引用!)的->*,让它检测到这一点并将其传递给lambda。这可以使它真正成为一种“任何方法”,因为它适用于使用该方法的变体,super_anys和指针。

有一点if constexpr工作,lambda可以分支在每种情况下进行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 <<)。

答案 1 :(得分:7)

这是我的解决方案。它看起来比Yakk短,并且它不使用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;
  }
};

操作符 - &GT; *:

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
}

Live