gcc和clang STL实现是否违反了有关分配器的规则?

时间:2014-11-10 20:24:16

标签: c++ gcc clang

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

  

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


allocator_traits<allocator_type>::construct只调用传递的allocator的construct方法,如果allocator定义了一个方法。我尝试使用它并创建分配器,它使用列表初始化进行构造,因此我可以利用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<T>::other::construct而不是Allocator::construct。而且,由于我们从std::allocator继承,因此rebind给出std::allocator<T>::construct。好的,没问题,只需添加

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

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

这2个问题是违规行为还是我错过了什么?

2 个答案:

答案 0 :(得分:1)

据我了解,libstdc ++和MSVC ++在这里是正确的。正如注释所示,rebind的要点是,可能需要容器来构造非T的东西。例如,std::list<T>需要构建一个包含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';
}

答案 1 :(得分:0)

经过一些研究,我自己找到了答案并希望提供答案。

对于第一个问题(使用Allocator::rebind<T>::other::construct而不是Allocator::construct),我的第一个分配器实现(第二个是OK)不满足Allocator部分的rebind要求},见17.6.3.5表28:

+------------------+-------------+-------------------------------------+
|    Expression    | Return type | Assertion/note pre-/post- condition |
+------------------+-------------+-------------------------------------+
| typename         | Y           | For all U (including T),            |
| X::template      |             | Y::template rebind<T>::other is X.  |
| rebind<U>::other |             |                                     |
+------------------+-------------+-------------------------------------+

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

但是,引用的标准要求,该容器必须为完全 construct调用allocator_type函数而不是某些重新绑定类型,似乎是标准缺陷({{3} })。 std::setmultisetmapmultimap的Libstdc ++实现依赖于此事实,并使用rebinded allocator for construct(http://cplusplus.github.io/LWG/lwg-active.html#2218)。