【问题标题】:Avoiding default construction of elements in standard containers避免在标准容器中默认构建元素
【发布时间】:2011-11-05 07:45:35
【问题描述】:

我有兴趣构建一个uninitialized_vector 容器,它在语义上与std::vector 相同,但需要注意的是,否则将使用无参数构造函数创建的新元素将在没有初始化的情况下创建。我主要感兴趣的是避免将 POD 初始化为 0。据我所知,没有办法通过将 std::vector 与特殊类型的分配器结合使用来实现此目的。

我想以与std::stack 相同的方式构建我的容器,它适应用户提供的容器(在我的例子中,std::vector)。换句话说,我想避免重新实现整个std::vector,而是在它周围提供一个“外观”。

有没有一种简单的方法可以从std::vector 的“外部”控制默认构造?


这是我得到的解决方案,这启发了 Kerrek 的回答:

#include <iostream>
#include <vector>
#include <memory>
#include <algorithm>
#include <cassert>

// uninitialized_allocator adapts a given base allocator
// uninitialized_allocator's behavior is equivalent to the base
// except for its no-argument construct function, which is a no-op
template<typename T, typename BaseAllocator = std::allocator<T>>
  struct uninitialized_allocator
    : BaseAllocator::template rebind<T>::other
{
  typedef typename BaseAllocator::template rebind<T>::other super_t;

  template<typename U>
    struct rebind
  {
    typedef uninitialized_allocator<U, BaseAllocator> other;
  };

  // XXX for testing purposes
  typename super_t::pointer allocate(typename super_t::size_type n)
  {
    auto result = super_t::allocate(n);

    // fill result with 13 so we can check afterwards that
    // the result was not default-constructed
    std::fill(result, result + n, 13);
    return result;
  }

  // catch default-construction
  void construct(T *p)
  {
    // no-op
  }

  // forward everything else with at least one argument to the base
  template<typename Arg1, typename... Args>
    void construct(T* p, Arg1 &&arg1, Args&&... args)
  {
    super_t::construct(p, std::forward<Arg1>(arg1), std::forward<Args>(args)...);
  }
};

namespace std
{

// XXX specialize allocator_traits
//     this shouldn't be necessary, but clang++ 2.7 + libc++ has trouble
//     recognizing that uninitialized_allocator<T> has a well-formed
//     construct function
template<typename T>
  struct allocator_traits<uninitialized_allocator<T> >
    : std::allocator_traits<std::allocator<T>>
{
  typedef uninitialized_allocator<T> allocator_type;

  // for testing purposes, forward allocate through
  static typename allocator_type::pointer allocate(allocator_type &a, typename allocator_type::size_type n)
  {
    return a.allocate(n);
  }

  template<typename... Args>
    static void construct(allocator_type &a, T* ptr, Args&&... args)
  {
    a.construct(ptr, std::forward<Args>(args)...);
  };
};

}

// uninitialized_vector is implemented by adapting an allocator and
// inheriting from std::vector
// a template alias would be another possiblity

// XXX does not compile with clang++ 2.9
//template<typename T, typename BaseAllocator>
//using uninitialized_vector = std::vector<T, uninitialized_allocator<T,BaseAllocator>>;

template<typename T, typename BaseAllocator = std::allocator<T>>
  struct uninitialized_vector
    : std::vector<T, uninitialized_allocator<T,BaseAllocator>>
{};

int main()
{
  uninitialized_vector<int> vec;
  vec.resize(10);

  // everything should be 13
  assert(std::count(vec.begin(), vec.end(), 13) == vec.size());

  // copy construction should be preserved
  vec.push_back(7);
  assert(7 == vec.back());

  return 0;
}

此解决方案的工作方式取决于特定供应商的编译器和 STL 的 std::vector 实现与 c++11 的一致性程度。

【问题讨论】:

  • 你能定义一个什么都不做的默认构造函数吗?我敢打赌编译器可能会内联或忽略它,尤其是打开优化。
  • @Doug T. - 这可能会解决默认构造函数的情况,但有趣的事情似乎发生在 resize 中——目前尚不清楚如何在不调用构造函数的情况下调用像 resize 这样的函数。
  • @Doug:需要注意的是,您不再是具有用户定义构造函数的 POD...

标签: c++ stl initialization


【解决方案1】:

不要在容器周围使用包装器,而是考虑在元素类型周围使用包装器:

template <typename T>
struct uninitialized
{
    uninitialized() { }
    T value;
};

【讨论】:

  • 这不是默认构造value吗?
  • 啊,没想到他只使用了 POD 类型。
  • 我认为像这样的聪明方法可能会奏效,但我还有保留 std::vector::value_typestd::vector::reference 等特征的额外要求。这些不应该暴露 unintialized&lt;T&gt;
  • vector&lt;uninitialized&lt;T&gt;&gt; 编写uninitialized_vector&lt;T&gt; 应该很简单;每个成员函数只需要顺从底层vector对应的成员函数,如果需要,封装对value的访问。
【解决方案2】:

我认为问题归结为容器对元素执行的初始化类型。比较:

T * p1 = new T;   // default-initalization
T * p2 = new T(); // value-initialization

标准容器的问题在于它们采用默认参数进行值初始化,如resize(size_t, T = T())。这意味着没有优雅的方法可以避免值初始化或复制。 (对于构造函数也是如此。)

即使使用标准分配器也不起作用,因为它们的中心 construct() 函数接受了一个值初始化的参数。你宁愿需要一个使用默认初始化的construct()

template <typename T>
void definit_construct(void * addr)
{
  new (addr) T;  // default-initialization
}

这样的东西将不再是符合标准的分配器,但您可以围绕这个想法构建自己的容器。

【讨论】:

  • 感谢您的回答,但我不明白您对 construct() 采用值初始化的参数的评论。为什么不能是空操作?
  • @Jared:我的意思是标准分配器在标准容器中的使用方式——它们的construct() 函数通常类似于construct(void *, const T &amp;),所以在构建过程中至少有一个副本,但我想无论如何,这个函数都会被一个值初始化的对象调用,比如construct(p, T()),所以你不能逃避标准容器内的值初始化。 (即使是 C++11 的分配器也不能帮助你,因为你不能多次“移动”一些东西......)
  • 所以基本上我是说你可以装配一个行为非常像vector 的容器,但不要调用标准分配器,你只需让它说new (addr) T; 而不是打电话给construct()。您可以保留分配器的其他方面(即内存分配)。
  • 有趣 - 看起来您的 resize(size_t, T = T()) 示例实际上不是标准的。需要std::vector 来区分resize(size_t)resize(size_t, const T &amp;)(对其构造函数也是如此)。因此,标准分配器似乎可以工作,具体取决于std::vector 的符合程度。
  • “标准”当然是指taken from a reliable source :-) 事实上,N3290 表示存在两个不同的签名resize(size_t)resize(size_t, const T &amp;)。所以resize(n) 可能已经按照你的意愿行事了。
【解决方案3】:

我不相信这可以通过包装一个向量(适用于每种类型)来实现,除非您在每次添加和删除操作上调整向量的大小。

如果您可以放弃包装 STL 容器,您可以通过在堆上保留一个 char 数组并为您要构造的每个对象使用放置 new 来做到这一点。这样,您就可以准确地控制对象的构造函数和析构函数何时被调用。

【讨论】:

    猜你喜欢
    • 2013-04-29
    • 2021-05-17
    • 1970-01-01
    • 2023-03-12
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多