【问题标题】:Is this behavior of vector::resize(size_type n) under C++11 and Boost.Container correct?C++11 和 Boost.Container 下 vector::resize(size_type n) 的这种行为是否正确?
【发布时间】:2014-01-28 11:32:13
【问题描述】:

我有一个 C++03 应用程序,其中 std::vector<T> 类型始终用作临时缓冲区。因此,它们经常使用std::vector<T>::resize() 调整大小,以确保它们足够大以在使用前容纳所需的数据。这个函数的C++03原型其实是:

void resize(size_type n, value_type val = value_type());

所以实际上在调用resize() 时,通过添加适当数量的val 副本来扩大向量。但是,通常我只需要知道vector 足够大以容纳我需要的数据;我不需要用任何值初始化它。复制构建新值只是浪费时间。

C++11 来救援(我认为):在其规范中,它将 resize() 拆分为两个重载:

void resize(size_type n); // value initialization
void resize(size_type n, const value_type &val); // initialization via copy

这非常符合 C++ 的理念:只为您想要的东西付费。不过,正如我所指出的,我的应用程序不能使用 C++11,所以当我遇到 Boost.Container 库时我很高兴,它的文档中有 indicates support for this functionality。具体来说,boost::container::vector<T>实际上有resize()的三个重载:

void resize(size_type n); // value initialization
void resize(size_type n, default_init_t); // default initialization
void resize(size_type n, const value_type &val); // initialization via copy

为了验证我理解了一切,我做了一个快速测试来验证 C++11 std::vector<T>boost::container::vector<T> 的行为:

#include <boost/container/vector.hpp>
#include <iostream>
#include <vector>

using namespace std;
namespace bc = boost::container;

template <typename VecType>
void init_vec(VecType &v)
{
    // fill v with values [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
    for (size_t i = 0; i < 10; ++i) v.push_back(i);
    // chop off the end of v, which now should be [1, 2, 3, 4, 5], but the other 5 values
    // should remain in memory
    v.resize(5);
}

template <typename VecType>
void print_vec(const char *label, VecType &v)
{
    cout << label << ": ";
    for (size_t i = 0; i < v.size(); ++i)
    {
        cout << v[i] << ' ';
    }
    cout << endl;
}

int main()
{
    // instantiate a vector of each type that we're going to test
    std::vector<int> std_vec;
    bc::vector<int> boost_vec;
    bc::vector<int> boost_vec_default;

    // fill each vector in the same way
    init_vec(std_vec);
    init_vec(boost_vec);
    init_vec(boost_vec_default);

    // now resize each vector to 10 elements in ways that *should* avoid reinitializing the new elements
    std_vec.resize(10);
    boost_vec.resize(10);
    boost_vec_default.resize(10, bc::default_init);

    // print each one out
    print_vec("std", std_vec);
    print_vec("boost", boost_vec);
    print_vec("boost w/default", boost_vec_default);    
}

在C++03模式下用g++4.8.1编译如下:

g++ vectest.cc
./a.out

产生以下输出:

std: 0 1 2 3 4 0 0 0 0 0 
boost: 0 1 2 3 4 0 0 0 0 0 
boost w/default: 0 1 2 3 4 5 6 7 8 9

这并不奇怪。我希望 C++03 std::vector&lt;T&gt; 用零初始化最后的 5 个元素。我什至可以说服自己为什么boost::container::vector&lt;T&gt; 会这样做(我假设它在 C++03 模式下模拟 C++03 的行为)。当我专门要求默认初始化时,我才得到了我想要的效果。但是,当我在 C++11 模式下重建时如下:

g++ vectest.cc -std=c++11
./a.out

我得到了这些结果:

std: 0 1 2 3 4 0 0 0 0 0 
boost: 0 1 2 3 4 0 0 0 0 0 
boost w/default: 0 1 2 3 4 5 6 7 8 9

完全一样!这导致了我的问题:

我认为在这种情况下我应该从三个测试中的每一个中看到相同的结果是不是错了?这似乎表明std::vector&lt;T&gt; 接口更改并没有真正产生任何影响,因为在对resize() 的最终调用中添加的5 个元素在前两种情况下仍然被初始化为零。

【问题讨论】:

标签: c++ boost c++11 vector


【解决方案1】:

不是答案,而是a lengthy addendum to Howard's:我使用的分配器适配器与霍华德的分配器工作原理基本相同,但更安全

  1. 它只插入值初始化而不是所有初始化,
  2. 它正确地默认初始化。
// Allocator adaptor that interposes construct() calls to
// convert value initialization into default initialization.
template <typename T, typename A=std::allocator<T>>
class default_init_allocator : public A {
  typedef std::allocator_traits<A> a_t;
public:
  template <typename U> struct rebind {
    using other =
      default_init_allocator<
        U, typename a_t::template rebind_alloc<U>
      >;
  };

  using A::A;

  template <typename U>
  void construct(U* ptr)
    noexcept(std::is_nothrow_default_constructible<U>::value) {
    ::new(static_cast<void*>(ptr)) U;
  }
  template <typename U, typename...Args>
  void construct(U* ptr, Args&&... args) {
    a_t::construct(static_cast<A&>(*this),
                   ptr, std::forward<Args>(args)...);
  }
};

【讨论】:

  • @HowardHinnant 谢谢 - 我在你上次发布这个分配器时偷了它,我很喜欢它,足以让它抵御 UB。
  • PS:我替换了 Casey 的分配器,得到的结果与我的答案显示的相同。 Casey 的分配器应该和我的性能一样,而且更安全。
  • en.cppreference.com/w/cpp/container/vector/resize 现在链接到这里。太酷了:)
  • using A::A; 有什么作用?
  • @TrentP std::allocator_traits&lt;T&gt;::construct(Alloc&amp; a, U* u, Args&amp;&amp;... args) 如果有效则代表a.construct(u, args...),否则等价于::new(u) U(args...)。由于调用者应该使用allocator_traitsstd::allocator 不需要实现成员construct,这相当于回退 - 因此它已被弃用。此处实现的适配器使用allocator_traits,而不是直接在底层分配器上调用construct,因此即使在适配不再实现constructstd::allocator 时,它仍然可以正常工作。
【解决方案2】:

与 C++11 resize 签名有一个小的功能差异,但您的测试不会暴露它。考虑这个类似的测试:

#include <iostream>
#include <vector>

struct X
{
    X() {std::cout << "X()\n";}
    X(const X&) {std::cout << "X(const X&)\n";}
};

int
main()
{
    std::vector<X> v;
    v.resize(5);
}

在 C++03 下打印:

X()
X(const X&)
X(const X&)
X(const X&)
X(const X&)
X(const X&)

但在 C++11 下它会打印:

X()
X()
X()
X()
X()

此更改的动机是为了更好地支持vector 中的不可复制(仅移动)类型。大多数情况下,包括您的情况,此更改没有任何区别。

有一种方法可以使用自定义分配器(您的编译器可能支持也可能不支持)在 C++11 中完成您想要的操作:

#include <iostream>
#include <vector>

using namespace std;

template <class T>
class no_init_alloc
    : public std::allocator<T>
{
public:
    using std::allocator<T>::allocator;

    template <class U, class... Args> void construct(U*, Args&&...) {}
};


template <typename VecType>
void init_vec(VecType &v)
{
    // fill v with values [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
    v.resize(10);
    for (size_t i = 0; i < 10; ++i) v[i] = i;  // Note this change!!!
    // chop off the end of v, which now should be [1, 2, 3, 4, 5], but the other 5 values
    // should remain in memory
    v.resize(5);
}

template <typename VecType>
void print_vec(const char *label, VecType &v)
{
    cout << label << ": ";
    for (size_t i = 0; i < v.size(); ++i)
    {
        cout << v[i] << ' ';
    }
    cout << endl;
}

int
main()
{
    std::vector<int, no_init_alloc<int>> std_vec;
    init_vec(std_vec);
    std_vec.resize(10);
    print_vec("std", std_vec);
}

应该输出:

std: 0 1 2 3 4 5 6 7 8 9 

no_init_alloc 只是拒绝进行任何初始化,这对int 来说很好,留下一个未指定的值。我不得不更改您的 init_vec 以使用赋值来初始化而不是使用构造。因此,如果您不小心,这可能会很危险/令人困惑。但是它确实避免了不必要的初始化。

【讨论】:

  • 如果您说using std::allocator&lt;T&gt;::construct 并从您的construct 中删除Args,难道不是更安全和同样快速吗? IE。只拦截对默认构造函数的调用?
  • @MarcMutz-mmutz:Casey 的分配器是解决这个问题的方法。它解决了您所说的安全问题。
  • 鉴于 std::allocator::construct() 在 c++17 中已弃用,是否有机会提供符合 c++-17 的版本?
  • no_init_alloc::construct 不依赖于std::allocator&lt;T&gt;::construct() 的存在。凯西的default_init_allocator::construct 也没有。所以一切都很好。请注意,only std::allocator&lt;T&gt;::construct() 已弃用,not allocator_traits&lt;Alloc&gt;::construct。也就是说,construct 仍然是分配器的可选 要求,如果分配器选择不覆盖该默认值,allocator_traits 仍然提供默认的construct。行为上的唯一区别是您不应再将 construct 称为 std::allocator 的成员。
  • 我已经用 gcc 7.3.1(在 Fedora 27 上)对此进行了测试,但它没有按预期工作 - 我得到这个输出:0 1 2 3 4 0 0 0 0 0。当我在 GCC 资源管理器中使用不同的编译器使用它时,我看到 memset 仍在被调用。与此相反,Casey 的适配器工作正常。也许与分配器用户有关,例如 std::vector 一直在调用 Allocator&lt;T&gt;::rebind::other&lt;U&gt; - 即使 U 与 T 相同。
【解决方案3】:

所以实际上在调用 resize() 时,通过添加适当数量的 val 副本来扩大向量。然而,通常我只需要知道向量是否足够大以容纳我需要的数据;我不需要用任何值初始化它。复制构建新值只是浪费时间。

不,不是真的。拥有一个实际上未构造的元素容器是没有意义的。我不确定除了零之外你还期望看到什么。未指定/未初始化的元素?这不是价值初始化的意思。

如果你需要 N 个元素,那么你应该有 N 个正确构造的元素,这就是 std::vector::resize 所做的。值初始化将对没有默认构造函数的对象进行零初始化,因此实际上它与您想要的相反,这是 less 安全和初始化而不是更多。

我建议你真正追求的是std::vector::reserve

这似乎表明std::vector&lt;T&gt; 接口更改并没有真正产生任何影响

它肯定有效果,只是不是你要找的那个。新的resize 重载是为了方便起见,这样当您只需要默认初始化甚至值初始化时,您就不必构建自己的临时文件。这对容器的工作方式并没有根本性的改变,即它们始终持有有效的类型实例

有效,但如果您离开它们,则处于未指定状态!

【讨论】:

  • @Jefffrey:难道不是undefined behavior 可以访问vector 的元素超过size() 吗?似乎我想要的是带有默认初始化的 Boost 容器版本的 resize 。
  • @LightnessRacesinOrbit:我当然不会尝试阅读它们。我引用了一些较早的答案,这些答案表明访问 reserve()-ed 元素(即使是写入)是未定义的行为。我不确定为什么我的方法看起来如此奇怪。例如,我有一个缓冲区,目前拥有 5000 个chars。我想调整它的大小以容纳 1000000 chars 而不先将所有这些 chars 初始化为零(如果 vector 之前已经那么大,那么内存已经存在)。我在 HPC 应用程序中工作,其中填充内存的时间/缓存污染不可忽略。
  • @LightnessRacesinOrbit - 我相信vector::reserve()不会改变vector::size()返回的值,这意味着试图访问通过reserve扩展的向量的元素() 通过数组运算符或 get() 方法超出向量的原始大小可能会产生异常。
  • @Bukes:不是“可能”:肯定是at()[],绝对不是。但是是的,关键是容器的概念大小与它为保存数据而在内部分配的内存量之间存在区别。 概念上“在”容器中的任何元素都必须已初始化,这就是 OP 出错的地方。我认为他可能能够在过渡期间使用预订来解决他的问题,因为他实际上并没有告诉我们问题出在哪里。 :) 如果这是一个预先分配的要求,reserve 是正确的。
  • @LightnessRacesinOrbit - 是的。值得一提的是,如果启用了检查迭代器(这是默认设置),则在尝试对超出范围的向量元素进行数组 [] 访问时,Microsoft 实现的最新年份将引发异常,请参阅:msdn.microsoft.com/en-us/library/aa985965.aspx
【解决方案4】:

未初始化的值

您可能已经通过创建适当的类来初始化值。 如下:

class uninitializedInt
{
public:
    uninitializedInt() {};
    uninitializedInt(int i) : i(i) {};

    operator int () const { return i; }

private:
    int i;
};

输出与“boost w/default”相同。

或者使用constructdestroy 作为nop 创建一个自定义分配器。

拆分resize原型

如果void std::vector&lt;T&gt;::resize(size_type n)void bc::vector&lt;T&gt;::resize(size_type n, default_init_t) 做的事情,那么很多旧的有效代码就会被破坏......


resize() 的拆分允许调整“仅移动”类的矢量大小,如下所示:

class moveOnlyInt
{
public:
    moveOnlyInt() = default;
    moveOnlyInt(int i) : i(i) {};

    moveOnlyInt(const moveOnlyInt&) = delete;
    moveOnlyInt(moveOnlyInt&&) = default;
    moveOnlyInt& operator=(const moveOnlyInt&) = delete;
    moveOnlyInt& operator=(moveOnlyInt&&) = default;

    operator int () const { return i; }
private:
    int i;
};

【讨论】:

    【解决方案5】:

    int 的值初始化产生 0。

    int 的默认初始化根本不初始化值 - 它只是保留内存中的任何内容。

    resize(10) 分配的内存没有被resize(5) 释放,或者相同的内存块被重用。无论哪种方式,您最终都会留下之前的内容。

    【讨论】:

      【解决方案6】:

      如果你想使用带有标准分配器的向量,这在 C++11 中不起作用吗??

          namespace{
             struct Uninitialised {};
      
             template<typename T>
             template<typename U>
             std::allocator<T>::construct(U* , Uninitialised&&)
             {
                /*do nothing*/
             }; 
          }
      
         template<typename T>
         void resize_uninitialised(std::vector<T>& vec, 
                                   std::vector<T>::size_type size)
         {
              const Uninitialised* p = nullptr;
              auto cur_size = vec.size();
      
              if(size <= cur_size)
                return;
      
              vec.reserve(size);
      
              //this should optimise to  vec.m_size += (size - cur_size);
              //one cannot help thinking there  must be simpler ways to do that. 
              vec.insert(vec.end(), p, p + (size - cur_size));
         };
      

      【讨论】:

        【解决方案7】:

        关于凯西回答的小记:

        正如 Casey 在 1. 中指出的那样,上面的代码只插入了值初始化。我不知道这是否需要,以下 - 但至少对我来说,这并不明显。

        上面的代码only避免了Plain Old Datatype [POD]-ed T in std::vector&lt;T, default_init_allocator&lt;T&gt;&gt; 的初始化。

        这是否真的避免了运行时开销,但目前我不知道。也许其他人可以为我回答这个问题。

        如果有人想对此进行测试,我将在下面附加我用于实际测试的代码。我应该补充一点,我使用了 set(CMAKE_CXX_STANDARD 11) 和 MinGW 8.1.0 64 位 C++ 编译器。

            // ----------------------------------------------------
        
        #include <iostream>
        #include <memory>
        #include <vector>
        
        // ----------------------------------------------------
        
        // forward declarations
        class Blub;
        std::ostream & operator<<(std::ostream &os, Blub const & blub);
        
        
        class Blub
        {
            static unsigned instanceCounter_;
        
         public:
            static unsigned constexpr NO_SOURCE = -1;
        
            // default constructor
            Blub()
                : instance(instanceCounter_)
                , instanceSource(NO_SOURCE)
            {
                std::cout << "default constructor: " << *this << std::endl;
                ++instanceCounter_;
            }
        
            // destructor
            ~Blub()
            {
                --instanceCounter_;
                std::cout << "destructor: " << *this << std::endl;
            }
        
            // copy constructor
            Blub(Blub const &other)
                : instance(instanceCounter_)
                , instanceSource(other.instance)
            {
                std::cout << "copy constructor: " << *this << std::endl;
                ++instanceCounter_;
            }
        
            // move constructor
            Blub(Blub &&other)
                : instance(std::move(other.instance))
                , instanceSource(std::move(other.instanceSource))
            {
                std::cout << "move constructor: " << *this  << std::endl;
            }
        
            // copy assignment
            Blub & operator=(Blub const &other)
            {
                instanceSource = other.instance;
                std::cout << "copy assignment: " << *this << std::endl;
                return (*this);
            }
        
            // move assignment
            Blub & operator=(Blub &&other)
            {
                instance = std::move(other.instance);
                instanceSource = std::move(other.instanceSource);
                std::cout << "move assignment: " << *this << std::endl;
                return (*this);
            }
        
            unsigned instance;
            unsigned instanceSource;
        };
        
        unsigned Blub::instanceCounter_;
        
        std::ostream & operator<<(std::ostream &os, Blub const & blub)
        {
            os << "Blub " << blub.instance;
            if (Blub::NO_SOURCE != blub.instanceSource)
            {
                os << " [from " << blub.instanceSource << "]";
            }
            return os;
        }
        
        // ----------------------------------------------------
        
        // Allocator adaptor that interposes construct() calls to
        // convert value initialization into default initialization.
        template <typename T, typename A = std::allocator<T>>
        class default_init_allocator : public A
        {
            typedef std::allocator_traits<A> a_t;
        
        public:
        
            template <typename U> struct rebind
            {
                using other = default_init_allocator<U, typename a_t::template rebind_alloc<U>>;
            };
        
            using A::A;
        
            template <typename U>
            void construct(U* ptr) noexcept(std::is_nothrow_default_constructible<U>::value)
            {
                ::new(static_cast<void*>(ptr)) U;
            }
        
            template <typename U, typename...Args>
            void construct(U* ptr, Args&&... args)
            {
                a_t::construct(static_cast<A&>(*this),
                               ptr, std::forward<Args>(args)...);
            }
        };
        
        // ----------------------------------------------------
        
        template <typename VecType>
        void print_vec(const char *label, VecType &v)
        {
            std::cout << label << ": ";
            for (size_t i = 0; i < v.size(); ++i)
            {
                std::cout << v[i] << " ";
            }
            std::cout << std::endl;
        }
        
        // ----------------------------------------------------
        
        int main()
        {
            {
                std::cout << "POD:" << std::endl;
        
                std::vector<int, default_init_allocator<int>> vec(10);
                // fill with values
                for (size_t i = 0; i < 10; ++i)
                {
                    vec[i] = i;
                }
                print_vec("initialized", vec);
        
                vec.resize(5);
                // this should not change value 5 to 9 in memory
                vec.resize(10);
                print_vec("resized", vec);
            }
        
            std::cout << std::endl;
        
            {
                std::cout << "C++ class:" << std::endl;
                std::vector<Blub, default_init_allocator<Blub>> vec(10);
                print_vec("initialized", vec);
        
                vec.resize(5);
                vec.resize(10);
                print_vec("resized", vec);
            }
        }
        
        // ----------------------------------------------------
        

        我从中得到的输出是:

        POD:
        initialized: 0 1 2 3 4 5 6 7 8 9 
        resized: 0 1 2 3 4 5 6 7 8 9 
        
        C++ class:
        default constructor: Blub 0
        default constructor: Blub 1
        default constructor: Blub 2
        default constructor: Blub 3
        default constructor: Blub 4
        default constructor: Blub 5
        default constructor: Blub 6
        default constructor: Blub 7
        default constructor: Blub 8
        default constructor: Blub 9
        initialized: Blub 0 Blub 1 Blub 2 Blub 3 Blub 4 Blub 5 Blub 6 Blub 7 Blub 8 Blub 9 
        destructor: Blub 5
        destructor: Blub 6
        destructor: Blub 7
        destructor: Blub 8
        destructor: Blub 9
        default constructor: Blub 5
        default constructor: Blub 6
        default constructor: Blub 7
        default constructor: Blub 8
        default constructor: Blub 9
        resized: Blub 0 Blub 1 Blub 2 Blub 3 Blub 4 Blub 5 Blub 6 Blub 7 Blub 8 Blub 9 
        destructor: Blub 0
        destructor: Blub 1
        destructor: Blub 2
        destructor: Blub 3
        destructor: Blub 4
        destructor: Blub 5
        destructor: Blub 6
        destructor: Blub 7
        destructor: Blub 8
        destructor: Blub 9
        

        PS:这个完整的答案才出现,因为我还不允许制作 cmets。否则我可能只会要求 Casey 用注释来扩展上面的答案,这基本上只适用于 POD。

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 2012-08-24
          • 1970-01-01
          • 2014-08-07
          • 1970-01-01
          • 2021-01-26
          • 1970-01-01
          • 2018-04-28
          • 2013-03-31
          相关资源
          最近更新 更多