这种技术可以用来创建异质函子的容器吗? [英] Can this technique for creating a container of heterogenous functors be salvaged?

查看:199
本文介绍了这种技术可以用来创建异质函子的容器吗?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

博客文章介绍了一种创建异构指针的容器。基本的技巧是创建一个简单的基类(即没有显式函数声明,没有数据成员,没有任何东西)和一个模板派生类存储 std :: function<> 具有任意签名的对象,然后使容器保持 unique_ptr 到基类的对象。代码也是在GitHub上提供

This blog post describes a technique for creating a container of heterogeneous pointers. The basic trick is to create a trivial base class (i.e. no explicit function declarations, no data members, nothing) and a templated derived class for storing std::function<> objects with arbitrary signatures, then make the container hold unique_ptrs to objects of the base class. The code is also available on GitHub.

我不认为这个代码可以变得鲁棒; std :: function<> 可以从一个lambda创建,它可能包括一个捕获,它可能包含一个非重要对象的副本对象,其析构函数必须被调用。当 Func_t 类型从 unique_ptr 删除时,从地图中删除,只有它的(微不足道的)析构函数将被调用,因此 std :: function<> 对象不会被正确删除。

I don't think this code can be made robust; std::function<> can be created from a lambda, which might include a capture, which might include a by-value copy of a nontrivial object whose destructor must be called. When the Func_t type is deleted by unique_ptr upon removal from the map, only its (trivial) destructor will be called, so the std::function<> objects never get properly deleted.

来自GitHub上的示例的case代码,具有非平凡类型,然后通过lambda中的值捕获并添加到容器中。在下面的代码中,从示例复制的部分在注释中注释;一切是我的。这可能是一个更简单的示范的问题,但我很努力,甚至得到一个有效的编译这个东西。

I've replaced the use-case code from the example on GitHub with a "non-trivial type" that is then captured by value inside a lambda and added to the container. In the code below, the parts copied from the example are noted in comments; everything else is mine. There's probably a simpler demonstration of the problem, but I'm struggling a bit to even get a valid compile out of this thing.

#include <map>
#include <memory>
#include <functional>
#include <typeindex>
#include <iostream>

// COPIED FROM https://plus.google.com/+WisolCh/posts/eDZMGb7PN6T
namespace {

  // The base type that is stored in the collection.
  struct Func_t {};
  // The map that stores the callbacks.
  using callbacks_t = std::map<std::type_index, std::unique_ptr<Func_t>>;
  callbacks_t callbacks;

  // The derived type that represents a callback.
  template<typename ...A>
    struct Cb_t : public Func_t {
      using cb = std::function<void(A...)>;
      cb callback;
      Cb_t(cb p_callback) : callback(p_callback) {}
    };

  // Wrapper function to call the callback stored at the given index with the
  // passed argument.
  template<typename ...A>
    void call(std::type_index index, A&& ... args)
    {
      using func_t = Cb_t<A...>;
      using cb_t = std::function<void(A...)>;
      const Func_t& base = *callbacks[index];
      const cb_t& fun = static_cast<const func_t&>(base).callback;
      fun(std::forward<A>(args)...);
    }

} // end anonymous namespace

// END COPIED CODE

class NontrivialType
{
  public:
    NontrivialType(void)
    {
      std::cout << "NontrivialType{void}" << std::endl;
    }

    NontrivialType(const NontrivialType&)
    {
      std::cout << "NontrivialType{const NontrivialType&}" << std::endl;
    }

    NontrivialType(NontrivialType&&)
    {
      std::cout << "NontrivialType{NontrivialType&&}" << std::endl;
    }


    ~NontrivialType(void)
    {
      std::cout << "Calling the destructor for a NontrivialType!" << std::endl;
    }

    void printSomething(void) const
    {
      std::cout << "Calling NontrivialType::printSomething()!" << std::endl;
    }
};

// COPIED WITH MODIFICATIONS
int main()
{
  // Define our functions.
  using func1 = Cb_t<>;

  NontrivialType nt;
  std::unique_ptr<func1> f1 = std::make_unique<func1>(
      [nt](void)
      {
        nt.printSomething();
      }
  );

  // Add to the map.
  std::type_index index1(typeid(f1));
  callbacks.insert(callbacks_t::value_type(index1, std::move(f1)));

  // Call the callbacks.
  call(index1);

  return 0;
}

这将产生以下输出(使用G ++ 5.1时没有优化) p>

This produces the following output (using G++ 5.1 with no optimization):

NontrivialType{void}
NontrivialType{const NontrivialType&}
NontrivialType{NontrivialType&&}
NontrivialType{NontrivialType&&}
NontrivialType{const NontrivialType&}
Calling the destructor for a NontrivialType!
Calling the destructor for a NontrivialType!
Calling the destructor for a NontrivialType!
Calling NontrivialType::printSomething()!
Calling the destructor for a NontrivialType!

我计数五个构造函数调用和四个析构函数调用。我认为表示我的分析是正确的 - 容器不能正确地销毁它拥有的实例。

I count five constructor calls and four destructor calls. I think that indicates that my analysis is correct--the container cannot properly destroy the instance it owns.

当我向 Func_t 添加虚拟 = default 析构函数时,我看到一个匹配的ctor / dtor调用:

Is this approach salvageable? When I add a virtual =default destructor to Func_t, I see a matching number of ctor/dtor calls:

NontrivialType{void}
NontrivialType{const NontrivialType&}
NontrivialType{NontrivialType&&}
NontrivialType{NontrivialType&&}
NontrivialType{const NontrivialType&}
Calling the destructor for a NontrivialType!
Calling the destructor for a NontrivialType!
Calling the destructor for a NontrivialType!
Calling NontrivialType::printSomething()!
Calling the destructor for a NontrivialType!
Calling the destructor for a NontrivialType!

...因此我认为是吗?

... so I think this change might be sufficient. Is it?

(注意:此方法的正确性(或缺乏)是独立的异质函数是一个好主意,在一些非常特殊的情况下,可能有一些优点,例如,当设计一个解释器;例如,一个Python类可以被认为只是异构函数的容器加上一个异构容器数据类型,但一般来说,我决定提出这个问题表示我认为这在很多情况下可能是个好主意。)

(Note: the correctness--or lack thereof--of this approach is independent of whether the idea of a container of heterogeneous functions is a good idea. In a few very specific cases, there may be some merit, for instance, when designing an interpreter; e.g., a Python class may be thought of as just such a container of heterogeneous functions plus a container of heterogeneous data types. But in general, my decision to ask this question does not indicate that I think this is likely to be a good idea in very many cases.)

推荐答案

这基本上是一个试图实现类型擦除并得到可怕的错误。

This is basically someone trying to implement type erasure and getting it horribly wrong.

虚拟析构函数。被删除的东西的动态类型是明显不是 Func_t ,所以如果析构函数不是虚拟的,它显然是UB。

Yes, you need a virtual destructor. The dynamic type of the thing being deleted is obviously not Func_t, so it's plainly UB if the destructor isn't virtual.

无论如何,整个设计完全打破了。

The whole design is completely broken, anyway.

类型擦除的点在于你有一堆不同类型的共享特性(例如可以用 int 并得到一个 double back),并且你想把它们变成一个具有那个特性的类型(例如 std :: function< double(int)> )。根据它的性质,类型擦除是一条单向的路:一旦你删除了类型,你不能得到它,而不知道是什么。

The point of type erasure is that you have a bunch of different types that share a common characteristic (e.g. "can be called with an int and get a double back"), and you want to make them into a single type that has that characteristic (e.g., std::function<double(int)>). By its nature, type erasure is a one-way street: once you have erased the type, you can't get it back without knowing what it is.

什么是擦除一个空的类意味着什么?没有什么,除了这是一件事。它是一个 std :: add_pointer_t< std :: common_type_t< std :: enable_if_t< true> ;, std :: void_t< int>>> code> void * ),模糊衣服模糊。

What does erasing something down to an empty class mean? Nothing, other than "it's a thing". It's a std::add_pointer_t<std::common_type_t<std::enable_if_t<true>, std::void_t<int>>> (more commonly known as void*), obfuscated in template clothing.

设计有很多其他问题。因为类型被擦除到空虚,它不得不恢复原始类型,以做任何有用的东西。但是你不能在不知道它是什么的情况下恢复原始类型,所以它最终使用传递给的参数类型来调用来推断存储在地图。这是很可笑的错误,因为 A ... ,它代表传递给的参数的类型和值类别,很不可能完全匹配 std :: function 的模板参数的参数类型。例如,如果你有一个 std :: function< void(int)> 存储在那里,你试图用 int x = 0;调用(/ * ... * /,x); ,它是未定义的行为。

There are plenty of other problems with the design. Because the type was erased into nothingness, it had to recover the original type in order to do anything useful with it. But you can't recover the original type without knowing what it is, so it ends up using the type of arguments passed to Call to infer the type of the thing stored in the map. That is ridiculously error-prone, because A..., which represents the types and value categories of the arguments passed to Call, is highly unlikely to match exactly the parameter types of std::function's template argument. For instance, if you have a std::function<void(int)> stored in there, and you tried to call it with a int x = 0; Call(/* ... */ , x);, it's undefined behavior. Go figure.

更糟糕的是,任何不匹配都隐藏在导致未定义行为的 static_cast 更难找到和修复。还有一个好奇的设计,需要用户传递 type_index ,当你知道什么类型,不管怎样,但它只是一个侧面演示当与所有其他问题使用此代码。

To make it worse, any mismatch is hidden behind a static_cast that causes undefined behavior, making it harder to find and fix. There's also the curious design that requires the user to pass a type_index, when you "know" what the type is anyway, but it's just a sideshow when compared to all the other problems with this code.

这篇关于这种技术可以用来创建异质函子的容器吗?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

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