在C ++ 11和Boost.Container下,vector :: resize(size_type n)的这种行为是否正确?

时间:2014-01-09 18:31:16

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

我有一个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<T>用零初始化最后的5个元素。我甚至可以说服自己boost::container::vector<T>为什么这样做(我认为它模仿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<T>接口更改实际上没有任何影响,因为在resize()的最终调用中添加的5个元素仍然在前两种情况下使用零进行初始化。

7 个答案:

答案 0 :(得分:56)

不是答案,而是a lengthy addendum to Howard's:我使用的分配器适配器与Howard的分配器基本相同,但是更安全,因为

  1. 它只介入值初始化而不是所有初始化,
  2. 它正确默认初始化。
  3. // 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)...);
      }
    };
    

答案 1 :(得分:20)

与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以使用赋值来初始化而不是使用构造。如果你不小心,这可能是危险/混乱。但是确实避免进行不必要的初始化。

答案 2 :(得分:4)

  

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

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

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

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

  

这似乎表明std::vector<T>界面更改确实没有任何影响

它肯定有效果,而不是你正在寻找的那个。新的resize重载是为了方便起见,因此您无需构建自己的临时值,只需要进行默认值甚至值初始化即可。这对于容器的工作方式并不是一个根本性的改变,即它们总是包含类型的有效实例。

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

答案 3 :(得分:3)

未初始化的值

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

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<T>::resize(size_type n)执行了void bc::vector<T>::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;
};

答案 4 :(得分:2)

int的值初始化产生0。

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

resize(10)分配的内存未被resize(5)释放,或者重用了相同的内存块。无论哪种方式,你最终得到了之前的内容。

答案 5 :(得分:1)

关于凯西回答的小提示:

正如 Casey 在 1. 下所指出的,上面的代码只对值初始化进行了干预。我不知道这是否需要以下内容 - 但至少对我来说,这并不明显。

以上代码避免在T中初始化Plain Old Datatype [POD]-ed std::vector<T, default_init_allocator<T>>

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

如果有人想测试这个,我将在下面附加我用于实际测试的代码。我应该补充一点,我使用了 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:这个完整的答案只是出现了,因为我还不允许发表评论。否则我可能只会要求 Casey 用注释来扩展上面的答案,这基本上只适用于 POD。

答案 6 :(得分:0)

如果你想使用带有标准分配器的向量,这不适用于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));
   };