【问题标题】:Do gcc and clang STL implementations violate rules about allocator?gcc 和 clang STL 实现是否违反有关分配器的规则?
【发布时间】:2014-11-10 20:24:15
【问题描述】:

在 23.2.1p3 C++11 标准中,我们可以阅读:

对于声明allocator_type 的受本子条款影响的组件,存储在这些组件中的对象应使用allocator_traits<allocator_type>::construct 函数构造并使用allocator_traits<allocator_type>::destroy 函数(20.6.8.2) 销毁。这些函数只为容器的元素类型调用,而不是容器使用的内部类型。 [ 注意:这意味着,例如,基于节点的容器可能需要构造包含对齐缓冲区的节点并调用construct 将元素放入缓冲区。 —尾注 ]


allocator_traits<allocator_type>::construct 只是调用传递的分配器的construct 方法,如果分配器定义了一个。我尝试使用它并创建分配器,它使用列表初始化进行构造,因此我可以利用emplace 进行聚合初始化:

#include <memory>
#include <vector>
#include <string>
#include <iostream>
#include <cmath>

template<typename T>
struct init_list_allocator : public std::allocator<T> {
    template<typename... Args>
    void construct(T* p, Args&&... args)
        { ::new((void *)p) T{std::forward<Args>(args)...}; }

    // Fix copy-constructors usage for aggregates
    void construct(T* p, T& copy_construct_arg)
        { std::allocator<T>::construct(p, copy_construct_arg); }

    void construct(T* p, const T& copy_construct_arg)
        { std::allocator<T>::construct(p, copy_construct_arg); }

    void construct(T* p, const T&& copy_construct_arg)
        { std::allocator<T>::construct(p, std::move(copy_construct_arg)); }

    void construct(T *p, T&& move_construct_arg)
        { std::allocator<T>::construct(p, std::move(move_construct_arg)); }
};

template<class T>
using improved_vector = std::vector<T, init_list_allocator<T>>;

struct A {
    int x;
    double y;
    const char* z;
};

int main()
{
    using namespace std;
    vector<string> strings;
    improved_vector<A> v;
    for (int i = 0; i < 21; ++i) {
        strings.emplace_back(to_string(i*i));
        v.emplace_back(i, sqrt(i), strings.back().c_str());
    };
    for (const auto& elem : v)
        cout << elem.x << ' ' << elem.y << ' ' << elem.z << '\n';
}


但是,至少在 gcc 和 clang 中,这是行不通的。问题是,他们的vector 实现使用Allocator::rebind&lt;T&gt;::other::construct 而不是Allocator::construct。而且,由于我们从std::allocator 继承,这个rebind 给出了std::allocator&lt;T&gt;::construct。好的,没问题,添加就行了

template<typename U>
struct rebind {
    using other = init_list_allocator<U>;
};

在我们分配器的定义中,此代码将起作用。太好了,现在让我们将 vector 更改为 list。这里我们遇到了无法解决的问题,因为不是Allocator::construct 对象在std::_List_node&lt;_Tp&gt; 构造函数中以直接初始化形式(带括号的形式)初始化。

这 2 个问题是违反标准还是我遗漏了什么?

【问题讨论】:

  • 第一个问题显然不是违规——嵌套重新绑定结构的全部目的是允许容器在必要时分配“包装器”。除了vector 之外,几乎每个容器都必须这样做。我在标准中没有看到任何地方禁止标准库实现对第二种情况使用直接初始化,但是我对此的第一个参考结果证明是不正确的,所以我现在删除了我的答案。
  • 对于第一个问题:我认为,在本段标准中,容器应使用allocator_type::construct 进行对象构造,其中每个容器的allocator_type 定义为typedef Allocator allocator_type,其中Allocator是容器的模板参数。所以使用rebind获取内存是可以的,但是对于我们的对象构造就不行了。我错了吗?
  • 这可能是 libstdc++ 中的一个错误,是的。你引用的那一点似乎表明了这一点。
  • 旁白:C++ 的设计并不是围绕要求对所有内容进行继承。继承是一种非常紧密的耦合关系,通常,要正确使用它,继承类需要在设计时考虑到它,并且继承类需要对父类有很好的理解。从std::allocator 继承对于实现分配器不是必需的,并且该类不是专门为它设计的。这是您可能不应该使用继承的一种情况。

标签: c++ gcc clang


【解决方案1】:

据我了解,libstdc++ 和 MSVC++ 在这里是正确的。正如注释所示,rebind 的意义在于,可能需要容器来构造不是T 的东西。例如std::list&lt;T&gt;需要构造一个包含T的列表节点,而不是T。关联容器和无序容器也存在类似情况。这就是为什么rebind 结构首先存在的原因。在此之前,您的分配器是不合格的。


关于第二期,你的参考

这些函数只为容器的元素类型调用,而不是容器使用的内部类型。

似乎表明标准库实现不允许为反弹分配器调用construct。这可能是 libstdc++ 中的一个错误。


至于这个问题的实际解决方案,给A一个具有你想要的行为的构造函数,并且不要为此使用分配器。人们可能希望使用特殊分配器在容器外创建A 的实例:

#include <vector>

struct A {
    int x;
    double y;
    const char* z;
    A() = default; // This allows A to still be a POD because the default constructor
                   // is not "user-provided", see 8.4.2 [dcl.fct.def.default]/4
    A(int x_, double y_, char const* z_) : x(x_), y(y_), z(z_) {}

};

int main()
{
    using namespace std;
    vector<string> strings;
    vector<A> v;
    for (int i = 0; i < 21; ++i) {
        strings.emplace_back(to_string(i*i));
        v.emplace_back(i, sqrt(i), strings.back().c_str());
    };
    for (const auto& elem : v)
        cout << elem.x << ' ' << elem.y << ' ' << elem.z << '\n';
}

【讨论】:

  • 对于第一期,请看我上面的评论。对于您的解决方案,我想创建一些通用的东西,它可以适用于任何聚合类型。
  • @Bevel:对于第一个问题,我坚持答案所说的。您必须提供rebind。通过反弹分配器(第二个问题)调用construct 可能不符合要求,但调用反弹分配器的任何其他成员是完全符合要求的。
  • 是的,我明白这一点,但是因为这里我的分配器没有实现任何棘手的内存分配技术,所以我可以继承rebind 的行为,只是想从我的分配器调用construct。我不同意的问题是construct通过rebind 调用,我对rebind 本身很好。
【解决方案2】:

经过一番研究,我自己找到了答案,并想提供它。

对于第一个问题(使用Allocator::rebind&lt;T&gt;::other::construct 而不是Allocator::construct),我的第一个分配器实现(第二个可以)在rebind 的一部分中不满足Allocator 要求,请参见17.6.3.5 表28 :

+-----------------+-------------+---------------- ---------------------+ |表达 |返回类型 |断言/注释前置/后置条件 | +-----------------+-------------+---------------- ---------------------+ |类型名 |是 |对于所有 U(包括 T),| | X::模板 | | Y::template rebind::other 是 X。| |重新绑定::other | | | +-----------------+-------------+---------------- ---------------------+

对于第二个问题:GCC 具有旧的 C++11 之前的 std::list 实现,仅在 GCC 5.0 中修复,因为此更改破坏了 ABI(请参阅 Should std::list::size have constant complexity in C++11? 了解更多信息)

但是,引用的标准要求,该容器必须为 完全 allocator_type 调用 construct 函数,而不是对于某些重新绑定的类型,这似乎是一个标准缺陷 (http://cplusplus.github.io/LWG/lwg-active.html#2218)。 std::setmultisetmapmultimap 的 Libstdc++ 实现依赖于这一事实,并使用重新绑定的分配器进行构造 (https://gcc.gnu.org/bugzilla/show_bug.cgi?id=64096)。

【讨论】:

    猜你喜欢
    • 2014-07-27
    • 2012-02-17
    • 2019-05-04
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2020-04-11
    • 1970-01-01
    相关资源
    最近更新 更多