关于Hinnant堆栈分配器的问题

Questions about Hinnant's stack allocator

本文关键字:问题 分配器 堆栈 Hinnant 关于      更新时间:2023-10-16

我一直在使用Howard Hinnant的堆栈分配器,它就像一个魅力,但是实现的一些细节对我来说有点不清楚。

  1. 为什么使用全球运营商newdeleteallocate()deallocate() 成员函数分别使用 ::operator new::operator delete。同样,成员函数construct()使用全局放置 new。为什么不允许任何用户定义的全局或特定于类的重载?
  2. 为什么对齐方式设置为硬编码的 16 字节而不是 std::alignment_of<T>
  3. 为什么构造函数和max_size具有throw()异常规范?这不是不鼓励这样做吗(例如,请参阅第 14 项C++更有效。当分配器中发生异常时,是否真的有必要终止和中止?新的 C++11 noexcept 关键字是否会改变这一点?
  4. construct()成员函数将是完美转发(到正在调用的构造函数(的理想候选项。这是编写 C++11 一致性分配器的方式吗?
  5. 还需要进行哪些其他更改才能使当前代码符合 C++11?

我一直在使用霍华德·欣南特的堆栈分配器,它可以工作 像一个魅力,但实现的一些细节有点 我不清楚。

很高兴它一直在为您工作。

1. 为什么使用全局运算符newdeleteallocate()deallocate() 成员函数使用 ::operator new 和 分别::operator delete。同样,成员函数 construct()使用全局放置 new。为什么不允许任何 用户定义的全局或特定于类的重载?

没有特别的原因。 请随意以最适合您的方式修改此代码。 这更像是一个例子,而且绝不是完美的。 唯一的要求是分配器和解除分配器提供正确对齐的内存,并且构造成员构造参数。

在 C++11 中,构造(和销毁(成员是可选的。 如果您在提供allocator_traits的环境中操作,我鼓励您从分配器中删除它们。 要找出答案,只需删除它们,看看是否仍然编译。

2. 为什么对齐方式设置为硬编码的 16 字节而不是 std::alignment_of<T> 字节?

std::alignment_of<T>可能会正常工作。 那天我可能是偏执狂。

3. 为什么构造函数和max_sizethrow()异常规范?这不是气馁吗(例如,请参阅更有效C++ 项目14.(?是否真的有必要在以下情况下终止和中止 分配器中发生异常?新 C++11 是否会更改此情况 noexcept关键词?

这些成员永远不会扔。 对于 C++11,我应该将它们更新为 noexcept. 在C++11中,用noexcept装饰事物变得更加重要,尤其是特殊成员。 在 C++11 中,可以检测表达式是否为 nothrow。 代码可以根据该答案进行分支。 已知为 nothrow 的代码更有可能导致泛型代码分支到更有效的路径。 std::move_if_noexcept是 C++11 中的规范示例。

永远不要使用throw(type1, type2)。 它已在 C++11 中弃用。

当你真的想说时,请使用throw():这永远不会抛出,如果我错了,请终止程序,以便我可以调试它。 throw() 也在 C++11 中被弃用,但有一个直接的替代品:noexcept

4. construct()成员函数将是完美转发(到正在调用的构造函数(的理想候选者。这是 编写 C++11 符合分配器的方法?

是的。 但是allocator_traits会为您做到这一点。 让它吧。 std::lib 已经为您调试了该代码。 C++11 个容器将调用allocator_traits<YourAllocator>::construct(your_allocator, pointer, args...) 。 如果分配器实现了这些函数,allocator_traits将调用您的实现,否则它将调用经过调试的高效默认实现。

5. 要使当前代码符合 C++11,还需要进行哪些其他更改?

说实话,这个分配器并不是真正的C++03或C++11。 复制分配器时,原始副本和副本应该彼此相等。 在这种设计中,这永远不会正确。 然而,这件事仍然碰巧在很多情况下都有效。

如果要使其严格符合,则需要另一个级别的间接寻址,以便副本将指向同一缓冲区。

除此之外,C++11 分配器比 C++98/03 分配器更容易构建。 以下是您必须做的最低限度:

template <class T>
class MyAllocator
{
public:
    typedef T value_type;
    MyAllocator() noexcept;  // only required if used
    MyAllocator(const MyAllocator&) noexcept;  // copies must be equal
    MyAllocator(MyAllocator&&) noexcept;  // not needed if copy ctor is good enough
    template <class U>
        MyAllocator(const MyAllocator<U>& u) noexcept;  // requires: *this == MyAllocator(u)
    value_type* allocate(std::size_t);
    void deallocate(value_type*, std::size_t) noexcept;
};
template <class T, class U>
bool operator==(const MyAllocator<T>&, const MyAllocator<U>&) noexcept;
template <class T, class U>
bool operator!=(const MyAllocator<T>&, const MyAllocator<U>&) noexcept;

您可以选择考虑MyAllocator可交换,并将以下嵌套类型放入分配器中:

typedef std::true_type propagate_on_container_swap;

还有其他一些类似的旋钮,您可以在 C++11 分配器上进行调整。 但是所有旋钮都有合理的默认值。

更新

上面我注意到我的堆栈分配器不符合,因为副本不相等。 我决定将此分配器更新为符合 C++11 分配器。 新的分配器称为short_allocator,在此处进行了记录。

short_allocator与堆栈分配器的不同之处在于,"内部"缓冲区不再是分配器的内部缓冲区,而是现在可以位于本地堆栈上的单独"竞技场"对象,也可以位于给定的线程或静态存储持续时间上。 arena不是线程安全的,所以要注意这一点。如果你愿意,你可以让它线程安全,但这会减少回报(最终你会重新发明 malloc(。

这是一致的,因为分配器的副本都指向相同的外部arena。 请注意,N 的单位现在是字节,而不是T数。

可以通过添加 C++98/03 样板(typedefs、构造成员、销毁成员等(将此 C++11 分配器转换为 C++98/03 分配器。 一项乏味但简单的任务。

新short_allocator对这个问题的答案保持不变。