类型擦除类型擦除,`any`问题? [英] Type erasing type erasure, `any` questions?

查看:123
本文介绍了类型擦除类型擦除,`any`问题?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

因此,假设我要键入erase 类型擦除



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

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

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

(var-> * print)(std :: cout); //打印它不知道是什么

我的问题是,如何扩展到 std :: any



无法在原始中进行。但是在我们分配给/构造一个 std :: any 的时候,我们有我们需要的类型信息。



因此,理论上,一个扩充的任何

  template< class ... OperationsToTypeErase> 
struct super_any {
std :: any data;
//或者一些OperationsToTypeErase的转换?
std :: tuple< OperationsToTypeErase ...>操作;
// ??什么for ctor / assign / etc?
};

可以以某种方式自动重新绑定一些代码,使上述类型的语法可以工作。

$

 模板< b> class ... Ops,class Op,
//操作匹配的SFINAE过滤器:
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.data);
}

现在我可以将此类型保存为类型



理想情况下,我需要:

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

使用printable_any = make_super_any<& print> ;;

printable_any bob = 7; //设置打印数据附加到任何

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

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

这是一个使用C ++ 14和 boost :: any

的解决方案。 code>,因为我没有C ++ 17编译器。



我们最终得到的语法是:

  const auto print = 
make_any_method< void(std :: ostream&)>([](auto& p,std :: ostream& ; t){t


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

super_any<& print> α= 7;

(a-> * print)(std :: cout);

在C ++ 17中,我可以通过使用 ... 而不是 decltype 噪声

的指向 any_method

任何公开继承是有点冒险,好像有人拿走任何顶部并修改它, any_method_data tuple 将过期。可能我们应该模仿整个任何接口,而不是继承公开。



@dyp写了一个概念证明评论。这是基于他的工作,清除了价值语义(偷走从 boost ::任何)添加。 @ cpplearner的基于指针的解决方案被用来缩短它(感谢!),然后我添加了vtable优化的顶部。






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

  template< class T> struct tag_t {constexpr tag_t {};}; 
template< class T> constexpr tag_t< T>标签{};

此trait类获取存储的签名 any_method



这将创建一个函数指针类型和一个工厂用于所述函数指针,给定 any_method

 模板< 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>
类型operator()(tag_t< T>)const {
return [](boost :: any& self,any_method const * method,Args ... args){
return方法)(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任何支持SFINAE的方法:

  template< class super_any,class method> 
struct super_method_applies:std :: false_type {};

template< class M0,class ...方法,类方法>
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

我们将构造的对象存储在 code> any_method 。请注意,如果使用非lambda构造它,那么这个 any_method 类型将用作调度机制的一部分

 模板< class Sig,F类> 
struct any_method {
using signature = Sig;

private:
F f;
public:

template< class Any,
// SFINAE测试其中一个Anys匹配此类型:
std :: enable_if_t< super_method_applies& std :: decay_t< Any>,any_method> {},int> * = nullptr
>
friend auto operator-> *(任何&&自己,any_method const& m){
//我们不使用any_method的值,因为每个any_method都有
/ /一个唯一类型(!),我们检查super_any
中的一个auto *是否已经有一个指向我们的指针。然后我们分派到相应的
// 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 。它是一个任何,它携带一堆类型擦除函数指针,每当包含任何 does:

  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
using dT = std :: decay_t< T> ;;
this-> change_type(tag< dT>);
return * this;
}
};因为我们将 any_method 存储为


$ b 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< \\\
;});
const auto wprint = make_any_method< void(std :: wostream&)>([](auto& p,std :: wostream& os){os<< p< \\\
;});

const auto wont_work = make_any_method< void(std :: ostream&)>([](auto& p,std :: ostream& t){t< <\\\
;});

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 {}; //如果尝试存储不可打印,则生成错误
}

实例



错误讯息当我尝试在 super_any 里面存储一个不可打印的 struct X {};


  main.cpp:150:87:错误:二进制表达式的操作数:ostream'(aka'basic_ostream< char>')和'X')
const auto x0 = make_any_method< void(std :: ostream& ostream& t){t


c



> any_method 的结构与 pseudo_method






我使用手动vtable来保持每个 super_any类型擦除开销为1个指针。这会为每个any_method调用添加重定向成本。我们可以非常容易地将指针直接存储在 super_any 中,并且不难将 super_any 。在任何情况下,在1擦除方法的情况下,我们应该直接存储它。






两个不同 any_method s的同一类型(比如说,都包含一个函数指针)生成同样的 super_any 。这会导致查找时出现问题。



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



这似乎超出了这个答案的范围,然而;






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



有一点如果constexpr 工作,lambda可以分支在每个情况下做ADL或方法调用。



这应该给我们:

 (7> * print)(std :: cout); 

((super_any<& print>)(7) - > * print)(std :: cout); // C ++ 17以上语法的版本

((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 )。


So, suppose I want to type erase type erasure.

I can create pseudo-methods for variants that enable a natural:

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

My question is, how do I extend this to a std::any?

It cannot be done "in the raw". But at the point where we assign to/construct a std::any we have the type information we need.

So, in theory, an augmented any:

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

could somehow automatically rebind some code such that the above type of syntax would work.

Ideally it would be as terse in use as the variant case is.

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

Now can I keep this to a type, yet reasonably use the lambda syntax to keep things simple?

Ideally I want:

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
}

or similar syntax. Is this impossible? Infeasible? Easy?

解决方案

This is a solution that uses C++14 and boost::any, as I don't have a C++17 compiler.

The syntax we end up with is:

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

which is almost optimal. With what I believe to be simple C++17 changes, it should look like:

constexpr any_method<void(std::ostream&)> print =
  [](auto&& p, std::ostream& t){ t << p << "\n"; };

super_any<&print> a = 7;

(a->*print)(std::cout);

In C++17 I'd improve this by taking a auto*... of pointers to any_method instead of the decltype noise.

Inheriting publicly from any is a bit risky, as if someone takes the any off the top and modifies it, the tuple of any_method_data will be out of date. Probably we should just mimic the entire any interface rather than inherit publicly.

@dyp wrote a proof of concept in comments to the OP. This is based off his work, cleaned up with value-semantics (stolen from boost::any) added. @cpplearner's pointer-based solution was used to shorten it (thanks!), and then I added the vtable optimization on top of that.


First we use a tag to pass around types:

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

This trait class gets the signature stored with an any_method:

This creates a function pointer type, and a factory for said function pointers, given an 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)... );
    };
  }
};

Now we don't want to store a function pointer per operation in our super_any. So we bundle up the function pointers into a 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 );
  }
};

we could specialize this for a cases where the vtable is small (for example, 1 item), and use direct pointers stored in-class in those cases for efficiency.

Now we start the super_any. I use super_any_t to make the declaration of super_any a bit easier.

template<class...methods>
struct super_any_t;

This searches the methods that the super any supports for 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>{}>
{};

This is the pseudo-method pointer, like print, that we create globally and constly.

We store the object we construct this with inside the any_method. Note that if you construct it with a non-lambda things can get hairy, as the type of this any_method is used as part of the dispatch mechanism.

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

A factory method, not needed in C++17 I believe:

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

This is the augmented any. It is both an any, and it carries around a bundle of type-erasure function pointers that change whenever the contained any does:

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

Because we store the any_methods as const objects, this makes making a super_any a bit easier:

template<class...Ts>
using super_any = super_any_t< std::remove_const_t<std::remove_reference_t<Ts>>... >;

Test code:

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.

The error message when I try to store a non-printable struct X{}; inside the super_any seems reasonable at least on 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"; });

this happens the moment you try to assign the X{} into the super_any<decltype(x0)>.

The structure of the any_method is sufficiently compatible with the pseudo_method that acts similarly on variants that they can probably be merged.


I used a manual vtable here to keep the type erasure overhead to 1 pointer per super_any. This adds a redirection cost to every any_method call. We could store the pointers directly in the super_any very easily, and it wouldn't be hard to make that a parameter to super_any. In any case, in the 1 erased method case, we should just store it directly.


Two different any_methods of the same type (say, both containing a function pointer) spawn the same kind of super_any. This causes problems at lookup.

Distinguishing between them is a bit tricky. If we changed the super_any to take auto* any_method, we could bundle all of the identical-type any_methods up in the vtable tuple, then do a linear search for a matching pointer if there are more than 1. The linear search should be optimized away by the compiler unless you are doing something crazy like passing a reference or pointer to which particular any_method we are using.

That seems beyond the scope of this answer, however; the existence of that improvement is enough for now.


In addition, a ->* that takes a pointer (or even reference!) on the left hand side can be added, letting it detect this and pass that to the lambda as well. This can make it truly an "any method" in that it works on variants, super_anys, and pointers with that method.

With a bit of if constexpr work, the lambda can branch on doing an ADL or a method call in every case.

This should give us:

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

with the any_method just "doing the right thing" (which is feeding the value to std::cout <<).

这篇关于类型擦除类型擦除,`any`问题?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆