9

我正在编写一个容器,并希望允许用户使用自定义分配器,但我不知道是否应该通过引用或值传递分配器。

是否可以保证(或至少是一个合理的假设)分配器对象不会直接包含其内存池,因此可以复制分配器并期望分配器的内存池是交叉兼容的?还是我总是需要通过引用传递分配器?

(我发现通过引用传递对性能的影响大于 2,因为编译器开始担心别名,所以它决定了我是否可以依赖这个假设。)

4

2 回答 2

11

旧的 C++ 标准对符合标准的分配器提出了要求:这些要求包括,如果您有Alloc<T> a, b,则a == b,并且您可以b使用a. 分配器基本上是无状态的。


在 C++11 中,情况变得更加复杂,因为现在支持有状态分配器。当您复制和移动对象时,如果分配器不同,是否可以从另一个容器复制或移动一个容器,以及如何复制或移动分配器,都有特定的规则。

先回答你的问题:不,你绝对不能假设复制你的分配器是有意义的,你的分配器甚至可能不可复制。

这是关于这个主题的 23.2.1/7:

除非另有说明,本节中定义的所有容器都使用分配器获取内存(见 17.6.3.5)。allocator_traits<allocator_-type>::select_on_container_copy_construction这些容器类型的复制构造函数通过调用它们的第一个参数来获取分配器。移动构造函数通过移动构造从属于被移动容器的分配器中获取分配器。分配器的这种移动构造不应通过异常退出。这些容器类型的所有其他构造函数都采用Allocator&参数(17.6.3.5),一个分配器,其值类型与容器的值类型相同。[注意:如果构造函数的调用使用可选分配器参数的默认值,则分配器类型必须支持值初始化。—end note] 在每个容器对象的生命周期内或直到分配器被替换之前,此分配器的副本用于由这些构造函数和所有成员函数执行的任何内存分配。分配器只能通过 assignment 或 swap() 替换。只有当 allocator_traits<allocator_type>::propagate_on_container_copy_assignment::value, allocator_traits<allocator_type>::propagate_on_container_move_assignment::value, 或allocator_traits<allocator_type>::propagate_on_container_swap::value在执行相应的容器操作时为真。allocator_traits<allocator_type>::propagate_on_container_swap::value除非被交换的对象具有比较相等或为真的分配器,否则调用容器的交换函数的行为是未定义的。在本条款中定义的所有容器类型中,成员get_allocator()返回用于构造容器的分配器的副本,或者,如果该分配器已被替换,则返回最近替换的副本。

另请参见std::allocator_traits概要的文档。

于 2012-07-28T18:38:31.060 回答
11

在 C++11 第 17.6.3.5 节分配器要求 [allocator.requirements] 中指定了符合分配器的要求。其中的要求是:

X                    an Allocator class for type T
...
a, a1, a2            values of type X&
...
a1 == a2             bool          returns true only if storage
                                   allocated from each can be
                                   deallocated via the other.
                                   operator== shall be reflexive,
                                   symmetric, and transitive, and
                                   shall not exit via an exception.
...
X a1(a);                           Shall not exit via an exception.
                                   post: a1 == a

即,当您复制分配器时,需要两个副本才能删除彼此的指针。

可以想象,可以将内部缓冲区放入分配器,但副本必须保留其他缓冲区的列表。或者,分配器可能有一个不变量,即解除分配始终是无操作的,因为指针始终来自内部缓冲区(来自您自己的缓冲区或来自其他副本)。

但无论采用何种方案,副本都必须是“交叉兼容的”。

更新

这是一个符合 C++11 的分配器,它执行“短字符串优化”。为了使其符合 C++11,我必须将“内部”缓冲区放在分配器外部,以便副本相等:

#include <cstddef>

template <std::size_t N>
class arena
{
    static const std::size_t alignment = 16;
    alignas(alignment) char buf_[N];
    char* ptr_;

    std::size_t 
    align_up(std::size_t n) {return n + (alignment-1) & ~(alignment-1);}

public:
    arena() : ptr_(buf_) {}
    arena(const arena&) = delete;
    arena& operator=(const arena&) = delete;

    char* allocate(std::size_t n)
    {
        n = align_up(n);
        if (buf_ + N - ptr_ >= n)
        {
            char* r = ptr_;
            ptr_ += n;
            return r;
        }
        return static_cast<char*>(::operator new(n));
    }
    void deallocate(char* p, std::size_t n)
    {
        n = align_up(n);
        if (buf_ <= p && p < buf_ + N)
        {
            if (p + n == ptr_)
                ptr_ = p;
        }
        else
            ::operator delete(p);
    }
};

template <class T, std::size_t N>
class stack_allocator
{
    arena<N>& a_;
public:
    typedef T value_type;

public:
    template <class U> struct rebind {typedef stack_allocator<U, N> other;};

    explicit stack_allocator(arena<N>& a) : a_(a) {}
    template <class U>
        stack_allocator(const stack_allocator<U, N>& a)
            : a_(a.a_) {}
    stack_allocator(const stack_allocator&) = default;
    stack_allocator& operator=(const stack_allocator&) = delete;

    T* allocate(std::size_t n)
    {
        return reinterpret_cast<T*>(a_.allocate(n*sizeof(T)));
    }
    void deallocate(T* p, std::size_t n)
    {
        a_.deallocate(reinterpret_cast<char*>(p), n*sizeof(T));
    }

    template <class T1, std::size_t N1, class U, std::size_t M>
    friend
    bool
    operator==(const stack_allocator<T1, N1>& x, const stack_allocator<U, M>& y);

    template <class U, std::size_t M> friend class stack_allocator;
};

template <class T, std::size_t N, class U, std::size_t M>
bool
operator==(const stack_allocator<T, N>& x, const stack_allocator<U, M>& y)
{
    return N == M && &x.a_ == &y.a_;
}

template <class T, std::size_t N, class U, std::size_t M>
bool
operator!=(const stack_allocator<T, N>& x, const stack_allocator<U, M>& y)
{
    return !(x == y);
}

它可以这样使用:

#include <vector>

template <class T, std::size_t N> using A = stack_allocator<T, N>;
template <class T, std::size_t N> using Vector = std::vector<T, stack_allocator<T, N>>;

int main()
{
    const std::size_t N = 1024;
    arena<N> a;
    Vector<int, N> v{A<int, N>(a)};
    v.reserve(100);
    for (int i = 0; i < 100; ++i)
        v.push_back(i);
    Vector<int, N> v2 = std::move(v);
    v = v2;
}

上述问题的所有分配都是从arena大小为 1 Kb 的本地中提取的。您应该能够通过值或引用传递此分配器。

于 2012-07-28T19:06:11.193 回答