【问题标题】:Type erasing type erasure, `any` questions?类型擦除类型擦除,“任何”问题?
【发布时间】:2016-08-08 18:04:45
【问题描述】:

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

我可以为启用自然的变体创建伪方法:

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

现在我可以将其保留为 type,同时合理地使用 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
}

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

【问题讨论】:

  • 相切相关;您要针对什么实现进行测试?是否有任何主要的标准库有现成的版本?
  • 我觉得当我过去尝试做类似的事情时,我最终意识到本​​质上一切都回到了模板化虚拟,以及语言不允许它们的事实。我确信某些事情是可能的,但由于这个原因,许多更好的解决方案肯定是不可能的。
  • 我不确定我是否将所有部分拼凑在一起,但这是一个非常的粗略草图:coliru.stacked-crooked.com/a/2ab8d7e41d24e616
  • 通过运算符重载稍微细化:coliru.stacked-crooked.com/a/23a25da83c5ba11d
  • 当我阅读带有variant和operator->*的文档示例时,我不看名字就知道是你

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


【解决方案1】:

这是一个使用 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 中,我会通过使用指向 any_method 的指针的 auto*... 而不是 decltype 噪声来改进这一点。

any公开继承有点冒险,好像有人把any从顶部取下来并修改它,any_method_datatuple就会过时。或许我们应该模仿整个any 接口而不是公开继承。

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


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

template<class T>struct tag_t{constexpr tag_t(){};};
template<class T>constexpr tag_t<T> tag{};

这个 trait 类获取使用 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;

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

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

这是我们全局创建的伪方法指针,如printconstly。

我们将构造此对象的对象存储在any_method 中。请注意,如果您使用非 lambda 构造它,事情可能会变得很复杂,因为 any_methodtype 被用作调度机制的一部分。

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_methods 存储为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
}

live example.

当我尝试在super_any 中存储不可打印的struct X{}; 时出现的错误消息至少在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&lt;decltype(x0)&gt; 时,就会发生这种情况。

any_method 的结构与pseudo_method 充分兼容,pseudo_method 在变体上的作用相似,它们可能被合并。


我在这里使用手动 vtable 将类型擦除开销保持在每个 super_any 的 1 个指针。这为每个 any_method 调用增加了重定向成本。我们可以很容易地将指针直接存储在super_any 中,并且将其作为super_any 的参数并不难。无论如何,在1擦除方法的情况下,我们应该直接存储它。


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

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

但是,这似乎超出了此答案的范围;现在,这种改进的存在就足够了。


此外,可以添加一个-&gt;*,它在左侧接受一个指针(甚至是引用!),让它检测到这一点并将其传递给 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 &lt;&lt;)。

【讨论】:

  • 我认为应该可以使用虚函数而不是函数指针,方法是构造一个派生自在super_any_t::set_operation_to 中创建必要代码的类模板实例化的新类型。对于多重继承,这甚至应该与函数指针的分配一样短。由于您将输入函数限制为无状态/纯,因此每个 输入函数类型列表 将这些指针存储在 vtable 中一次似乎是可能的。
  • “因为我没有 C++17 编译器。”您的意思是除了 Wandbox 和任何其他在线编译器之外?另外:apt.llvm.org。
  • @TemplateRex 我认为 Yakk 的意思是没有一个编译器在这里完全支持 C++17 自动模板参数...
  • @dyp 手动 vtable,我们在其中创建指向静态 tuple 的指针,该静态tuple 在每个类型存储的基础上创建,会更好。每个 super any 实例一个指针的开销,而不是每个实例每个方法一个指针的开销。这避免了使用new(无论是否放置)。放置 new 不好,因为它需要“运气”才能获得正确的大小(或静态断言或非疯狂编译器),除非非常小心,否则会增加间接程度。
  • 这可以摆脱any_method_function中的签名吗?那么可以使用模板参数吗?就像 std::variant 和 std::visit 一样。
【解决方案2】:

这是我的解决方案。它看起来比 Yakk 的短,而且它没有使用std::aligned_storage 和placement new。它还支持有状态和局部函子(这意味着可能永远不可能写出super_any&lt;&amp;print&gt;,因为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
}

Live

【讨论】:

  • Slick,使用函数指针的签名,包括any_method 的类型,但它从根本上将您限制为唯一类型的any_methods(至少在同一个对象上)。我看不到一种方法可以轻松迁移到super_any&lt;&amp;print&gt; 中的auto* 指针参数。我猜想涉及auto* 标签的黑客攻击。我想这是一个极端情况,因为我们需要在 any_method 中进行编译时多态性,而 lambda 中的 auto&amp;&amp; self 是最简单的方法。比我的小很多! (+1)
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2012-05-28
  • 1970-01-01
  • 1970-01-01
  • 2020-04-17
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多