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

Type erasure and allocators: what's the expected behavior?

我在 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 之一(它没有分配器的类型作为其类型的一部分,但在构造期间仍然接受分配器)。

上面的代码可以编译,但我怀疑这个 class 是否按预期工作。
换句话说,每当 class 的用户提供自己的分配器时,它都会用作新 std::vector 的参数,其类型会被擦除,但不会用于分配实例由 ptr.

指向的 D

这是一个 valid/logical 设计,还是每次分配都应该一致地使用分配器? 我的意思是,是在 STL 或其他一些主要库中也可以找到的东西,还是没有多大意义的东西?

是的,这是一个有效的设计如果:

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

但是:

  1. 显然你会想要根据智能指针实现 ptr,或者非常小心地编写你所有的 copy/move constructors/operators.

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

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

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 在指针类型之间进行转换,这被保留为reader.)

练习