键入擦除和分配器:预期的行为是什么?

时间:2016-03-16 06:49:16

标签: c++ templates type-erasure allocator

我在codereview上问了同样的问题,但是他们恭敬地指出这个问题更适合SO。

请考虑以下代码:

#include<vector>
#include<memory>

template<typename T>
struct S final {
    struct B {
        virtual void push_back(T&& v) = 0;
        virtual ~B() { }
    };

    template<class Allocator>
    struct D final: public B {
        D(Allocator alloc): vec{alloc} { }
        void push_back(T&& v) override { vec.push_back(v); }
        std::vector<T, Allocator> vec;
    };

    S(): S{std::allocator<T>{}} { } 

    template<class Allocator>
    S(Allocator alloc): ptr{new D<Allocator>{alloc}} { }

    ~S() { delete ptr; }

    void push_back(T&& v) { ptr->push_back(std::move(v)); }

    B* ptr;
};

int main() {
    int x = 42;
    S<int> s1{};
    S<double> s2{std::allocator<double>{}}; 
    s1.push_back(42);
    s2.push_back(x);
}

这是问题目的的最小例子 想法是 type-erase 接受自定义分配器的东西(在本例中为std::vector),以便弯曲容器的定义(具有分配器的类型)作为其类型的一部分)类似于std::function之类的东西(它没有作为其类型的一部分的分配器的类型,但在构造期间仍然接受分配器)。

上面的代码编译,但我怀疑这个类是否按照预期的那样工作 换句话说,每当类的用户提供自己的分配器时,它就被用作新std::vector的参数,其类型被擦除,但它不用于分配{{1的实例D指出。

这是有效/逻辑设计,还是应该为每个分配一致地使用分配器? 我的意思是,在STL或其他一些主要的图书馆中也可以找到一些东西,或者它是没有多大意义的东西?

2 个答案:

答案 0 :(得分:1)

是的,这是一个有效的设计IF:

您希望内存分配策略是用户定义的,但希望类的接口具有多态性。例如,如果您的某些对象是由消息传递协议生成的,那么这将是合理的。在任何一条消息中,出于性能原因,可能有许多对象可以合理地从同一个内存块(由消息拥有)中分配。

然而:

  1. 显然你想要在智能指针方面实现ptr,或者非常谨慎地编写所有的复制/移动构造函数/运算符。

  2. 您需要非常仔细地管理从一个类型擦除的容器到另一个容器的复制(并且肯定会移动!)对象。例如,允许将分配器A分配的对象移动到由分配器B管理的容器中可能是无效的。这种事情很快就难以推理。需要进行运行时检查,如果违反(或者可能降级为副本?),可能会抛出std::logic_error,等等。

答案 1 :(得分:1)

没有正确的答案,设计是合理的,但使用用户提供的分配器来创建派生对象也是合理的。为此,您需要在类型擦除的上下文中进行销毁和释放,因此可以使用分配器:

template<typename T>
struct S final {
    struct B {
        // ...
        virtual void destroy() = 0;
    protected:
        virtual ~B() { }
    };

    template<class Allocator>
    struct D final: public B {
        // ...
        void destroy() override {
            using A2 = std::allocator_traits<Allocator>::rebind_alloc<D>;
            A2 a{vec.get_allocator()};
            this->~D();
            a2.deallocate(this, 1);
        }
    };

    S(): S{std::allocator<T>{}} { } 

    template<class Allocator>
      S(Allocator alloc): ptr{nullptr} {
          using DA = D<Allocator>;
          using AT = std::allocator_traits<Allocator>;
          static_assert(std::is_same<typename AT::pointer, typename AT::value_type*>::value, "Allocator doesn't use fancy pointers");

          using A2 = AT::rebind_alloc<DA>;
          A2 a2{alloc};
          auto p = a2.allocate(1);
          try {
              ptr = ::new((void*)p) DA{alloc};
          } catch (...) {
              a2.deallocate(p);
              throw;
          }
      }

    ~S() { ptr->destroy(); }

    // ...
};

(此代码断言Allocator::pointerAllocator::value_type*,以支持那些不正确的分配器,您需要使用pointer_traits来转换指针类型,这是剩下的作为读者的练习。)