考虑到副本构造的要求,我如何在C++11中编写一个有状态分配器

How can I write a stateful allocator in C++11, given requirements on copy construction?

本文关键字:分配器 状态 一个 C++11 副本 考虑到      更新时间:2023-10-16

据我所知,要与STL一起使用的分配器的要求容器在C++11标准第17.6.3.5节的表28中列出。

我对其中一些需求之间的交互有点困惑。给定类型X是类型T的分配器,类型Y是"的类型U、实例aa1a2XY的实例b,表中显示:

  1. 只有在分配了存储时,表达式a1 == a2才计算为true来自a1的分配可以由a2解除分配,反之亦然。

  2. 表达式CCD_ 15形式良好,不会通过异常退出,之后CCD_ 16为真。

  3. 表达式X a(b)格式良好,不会通过异常退出,并且之后为CCD_ 18。

我读到这里是说,在这样的复印件和原件可以互换的方式。更糟糕的是,同样跨类型边界为true。这似乎是一项相当繁重的要求;像据我所知,它使得大量类型的分配器变得不可能。

例如,假设我有一个freelist类,我想在分配器中使用它,以便缓存释放的对象。除非我错过了什么,否则我不会在分配器中包括该类的实例,因为大小或TU的排列可能不同,因此自由列表条目为不兼容。

我的问题:

  1. 我以上的解释正确吗?

  2. 我在一些地方读到C++11改进了对"有状态"的支持分配器"。考虑到这些限制,情况如何?

  3. 你对如何做我想做的事情有什么建议吗做也就是说,如何在分配器中包含已分配的类型特定状态?

  4. 一般来说,关于分配器的语言似乎很草率。(例如表28的序言假设a属于X&类型,但是表达式重新定义a。(此外,至少GCC的支持是不符合要求的。是什么导致了分配器的这种怪异?只是偶尔使用过的功能?

分配器的相等并不意味着它们必须具有完全相同的内部状态,只是意味着它们都必须能够释放使用任一分配器分配的内存X型分配器aY型分配器b的分配器a == b的交叉型相等性在表28中定义为"与a == Y::template rebind<T>::other(b)相同"。换句话说,由a分配的a == b如果内存可以由分配器解除分配,分配器通过将b重新绑定到avalue_type来实例化。

您的自由列表分配器不需要能够解除分配任意类型的节点,您只需要确保FreelistAllocator<T>分配的内存可以由FreelistAllocator<U>::template rebind<T>::other解除分配。假设在大多数正常的实现中,FreelistAllocator<U>::template rebind<T>::otherFreelistAllocator<T>是相同的类型,这很容易实现。

简单示例(Coliru现场演示(:

template <typename T>
class FreelistAllocator {
    union node {
        node* next;
        typename std::aligned_storage<sizeof(T), alignof(T)>::type storage;
    };
    node* list = nullptr;
    void clear() noexcept {
        auto p = list;
        while (p) {
            auto tmp = p;
            p = p->next;
            delete tmp;
        }
        list = nullptr;
    }
public:
    using value_type = T;
    using size_type = std::size_t;
    using propagate_on_container_move_assignment = std::true_type;
    FreelistAllocator() noexcept = default;
    FreelistAllocator(const FreelistAllocator&) noexcept {}
    template <typename U>
    FreelistAllocator(const FreelistAllocator<U>&) noexcept {}
    FreelistAllocator(FreelistAllocator&& other) noexcept :  list(other.list) {
        other.list = nullptr;
    }
    FreelistAllocator& operator = (const FreelistAllocator&) noexcept {
        // noop
        return *this;
    }
    FreelistAllocator& operator = (FreelistAllocator&& other) noexcept {
        clear();
        list = other.list;
        other.list = nullptr;
        return *this;
    }
    ~FreelistAllocator() noexcept { clear(); }
    T* allocate(size_type n) {
        std::cout << "Allocate(" << n << ") from ";
        if (n == 1) {
            auto ptr = list;
            if (ptr) {
                std::cout << "freelistn";
                list = list->next;
            } else {
                std::cout << "new noden";
                ptr = new node;
            }
            return reinterpret_cast<T*>(ptr);
        }
        std::cout << "::operator newn";
        return static_cast<T*>(::operator new(n * sizeof(T)));
    }
    void deallocate(T* ptr, size_type n) noexcept {
        std::cout << "Deallocate(" << static_cast<void*>(ptr) << ", " << n << ") to ";
        if (n == 1) {
            std::cout << "freelistn";
            auto node_ptr = reinterpret_cast<node*>(ptr);
            node_ptr->next = list;
            list = node_ptr;
        } else {
            std::cout << "::operator deleten";
            ::operator delete(ptr);
        }
    }
};
template <typename T, typename U>
inline bool operator == (const FreelistAllocator<T>&, const FreelistAllocator<U>&) {
    return true;
}
template <typename T, typename U>
inline bool operator != (const FreelistAllocator<T>&, const FreelistAllocator<U>&) {
    return false;
}

1( 我以上的解释正确吗?

您是对的,您的空闲列表可能不太适合分配器,它需要能够处理多个大小(和对齐(来适应。这是免费列表需要解决的问题。

2( 我在一些地方读到C++11改进了对"有状态分配器"的支持。考虑到这些限制,情况如何?

与其说它有多大的改善,不如说它是天生的。在C++03中,该标准只是促使实现者提供可以支持不平等实例和实现者的分配器,从而有效地使有状态分配器不可移植。

3( 你对如何做我想做的事情有什么建议吗?也就是说,如何在分配器中包含已分配的类型特定状态?

您的分配器可能必须是灵活的,因为您不应该确切地知道它应该分配什么内存(以及什么类型(。这个要求对于将您(用户(与使用分配器的某些容器(如std::liststd::setstd::map(的内部隔离是必要的。

您仍然可以将此类分配器与std::vectorstd::deque等简单容器一起使用。

是的,这是一项成本高昂的要求。

4( 一般来说,关于分配器的语言似乎很草率。(例如,表28的序言说假设a的类型为X&,但有些表达式重新定义了a。(此外,至少GCC的支持是不一致的。是什么导致了分配器的这种怪异?它只是一个不常用的功能吗?

一般来说,标准并不是很容易阅读,不仅仅是分配器。你必须小心。

老实说,gcc不支持分配器(它是一个编译器(。我猜想您说的是libstdc++(gcc附带的标准库实现(。libstdc++是旧的,因此它是为C++03量身定制的。它已经适应了C++11,但还没有完全一致(例如,仍然对字符串使用写时复制(。原因是libstdc++非常关注二进制兼容性,而C++11所需的许多更改将破坏这种兼容性;因此,必须谨慎地介绍它们。

我读到这里是说,所有分配器都必须是可复制的,这样副本才能与原件互换。更糟糕的是,跨类型边界也是如此。这似乎是一项相当繁重的要求;据我所知,它使得大量类型的分配器变得不可能。

如果分配器是某个内存资源的轻量级句柄,那么满足这些要求是很琐碎的。只是不要试图将资源嵌入到单独的分配器对象中。

例如,假设我有一个freelist类,我想在分配器中使用它来缓存释放的对象。除非我遗漏了什么,否则我不能在分配器中包含该类的实例,因为t和U的大小或对齐方式可能不同,因此自由列表条目不兼容。

[分配器要求]第9段:

分配器可以约束可以对其进行实例化的类型以及可以调用其construct成员的自变量。如果一个类型不能与特定的分配器一起使用,则分配器类或对construct的调用可能无法实例化。

分配器可以拒绝为除给定类型T之外的任何对象分配内存。这将防止它在基于节点的容器(如std::list(中使用,这些容器需要分配自己的内部节点类型(而不仅仅是容器的value_type(,但它对std::vector可以正常工作。

这可以通过防止分配器反弹到其他类型来实现:

class T;
template<typename ValueType>
class Alloc {
  static_assert(std::is_same<ValueType, T>::value,
    "this allocator can only be used for type T");
  // ...
};
std::vector<T, Alloc<T>> v; // OK
std::list<T, Alloc<T>> l;   // Fails

或者您只能支持适合sizeof(T):的类型

template<typename ValueType>
class Alloc {
  static_assert(sizeof(ValueType) <= sizeof(T),
    "this allocator can only be used for types not larger than sizeof(T)");
  static_assert(alignof(ValueType) <= alignof(T),
    "this allocator can only be used for types with alignment not larger than alignof(T)");
  // ...
};
  1. 我以上的解释正确吗

不完全是。

  1. 我在一些地方读到C++11改进了对"有状态分配器"的支持。考虑到这些限制,情况如何

C++11之前的限制甚至更糟!

现在,它清楚地指定了分配器在复制和移动时如何在容器之间传播,以及当分配器实例被可能与原始实例不相等的不同实例替换时,各种容器操作的行为。如果没有这些说明,就不清楚如果用有状态分配器交换两个容器会发生什么。

  1. 你对如何做我想做的事情有什么建议吗?也就是说,如何在分配器中包含已分配的类型特定状态

不要将它直接嵌入到分配器中,将其单独存储,并让分配器通过指针(可能是智能指针,这取决于您如何设计资源的生存期管理(来引用它。实际的分配器对象应该是某个外部内存源(例如竞技场、池或管理自由列表的东西(的轻量级句柄。共享同一源的分配器对象应该比较相等,即使对于具有不同值类型的分配器也是如此(见下文(。

我还建议,如果你只需要支持一种类型,就不要试图支持所有类型的分配。

  1. 一般来说,关于分配器的语言似乎很草率。(例如,表28的序言说假设a的类型为X&,但有些表达式重新定义了a。(

是的,正如您在https://github.com/cplusplus/draft/pull/334(谢谢(。

此外,至少GCC的支持是不符合要求的。

它不是100%,但会在下一个版本中出现。

是什么导致了分配器的这种怪异?它只是一个不常用的功能吗?

是的。而且有很多历史包袱,很难具体说明它是否广泛有用。我的ACCU 2012演示有一些细节,如果在阅读后你认为你可以让它变得更简单,我会非常惊讶;-(


关于分配器何时比较相等,请考虑:

MemoryArena m;
Alloc<T> t_alloc(&m);
Alloc<T> t_alloc_copy(t_alloc);
assert( t_alloc_copy == t_alloc ); // share same arena
Alloc<U> u_alloc(t_alloc);
assert( t_alloc == u_alloc ); // share same arena
MemoryArena m2
Alloc<T> a2(&m2);
assert( a2 != t_alloc ); // using different arenas

分配器相等的含义是,对象可以释放彼此的内存,因此,如果您从t_alloc分配一些内存,而(t_alloc == u_alloc)true,则意味着您可以使用u_alloc解除分配该内存。如果它们不相等,则u_alloc无法解除分配来自t_alloc的内存。

如果你只是有一个自由列表,其中任何内存都可以添加到任何其他自由列表中,那么你的所有分配器对象可能会相互比较相等。

相关文章: