Is this behavior of vector::resize(size_type n) un

2019-01-02 21:20发布

I have a C++03 application where std::vector<T> types are used throughout as temporary buffers. As such, they often get resized using std::vector<T>::resize() to ensure they are large enough to hold the required data before use. The C++03 prototype for this function is actually:

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

So in actuality when calling resize(), the vector is enlarged by adding the appropriate number of copies of val. Often, however, I just need to know that the vector is large enough to hold the data I need; I don't need it initialized with any value. Copy-constructing the new values is just a waste of time.

C++11 comes to the rescue (I thought): in its specification, it splits resize() into two overloads:

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

This fits nicely with the philosophy of C++: only pay for what you want. As I noted, though, my application can't use C++11, so I was happy when I came across the Boost.Container library, which indicates support for this functionality in its documentation. Specifically, boost::container::vector<T> actually has three overloads of 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

In order to verify that I understood everything, I whipped up a quick test to verify the behavior of C++11 std::vector<T> and 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);    
}

Compiling this with g++ 4.8.1 in C++03 mode as follows:

g++ vectest.cc
./a.out

yields the following output:

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

This isn't too surprising. I expect the C++03 std::vector<T> to initialize the final 5 elements with zeros. I can even convince myself why boost::container::vector<T> is doing the same (I would assume it emulates C++03 behavior in C++03 mode). I only got the effect that I wanted when I specifically ask for default initialization. However, when I rebuilt in C++11 mode as follows:

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

I get these results:

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

Exactly the same! Which leads to my question:

Am I wrong in thinking that I should see the same results from each of the three tests in this case? This seems to indicate that the std::vector<T> interface change hasn't really had any effect, as the 5 elements added in the final call to resize() still get initialized with zeros in the first two cases.

6条回答
笑指拈花
2楼-- · 2019-01-02 21:39

Value initialization of int yields 0.

Default initialization of int doesn't initialize the value at all - it just retains whatever was in memory.

Either the memory allocated by resize(10) wasn't released by resize(5), or the same memory block was reused. Either way you ended up with the prior contents left over.

查看更多
ら面具成の殇う
3楼-- · 2019-01-02 21:42

There is a small functional difference with the C++11 resize signatures, but your test will not expose it. Consider this similar test:

#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);
}

Under C++03 this prints:

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

But under C++11 it prints:

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

The motivation for this change is to better support non-copyable (move-only) types in vector. Most of the time, including in your case, this change makes no difference.

There is a way to accomplish what you want in C++11 with the use of a custom allocator (which your compiler may or may not yet support):

#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);
}

Which should output:

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

The no_init_alloc simply refuses to do any initialization, which is fine for int, leaving it with an unspecified value. I had to change your init_vec to use assignment to initialize instead of using construction though. So this can be dangerous / confusing if you are not careful. However it does avoid doing unnecessary initialization.

查看更多
泪湿衣
4楼-- · 2019-01-02 21:44

Not an answer, but a lengthy addendum to Howard's: I use an allocator adapter that basically works the same as Howard's allocator, but is safer since

  1. it only interposes on value-initialization and not all initializations,
  2. it correctly default-initializes.
// 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)...);
  }
};
查看更多
余生无你
5楼-- · 2019-01-02 21:50

So in actuality when calling resize(), the vector is enlarged by adding the appropriate number of copies of val. Often, however, I just need to know that the vector is large enough to hold the data I need; I don't need it initialized with any value. Copy-constructing the new values is just a waste of time.

No, not really. Having a container of elements that are not actually constructed does not make sense. I'm not sure what you expected to see other than zeroes. Unspecified/uninitialised elements? That's not what value-initialisation means.

If you need N elements, then you should have N properly-constructed elements, and that is what std::vector::resize does. Value-initialisation will zero-initialise an object with no default constructor to invoke, so really it's the opposite of what you seem to want, which is less safety and initialisation rather than more.

I suggest that what you're really after is std::vector::reserve.

This seems to indicate that the std::vector<T> interface change hasn't really had any effect

It certainly has an effect, just not the one you're looking for. The new resize overload is for convenience so that you don't have to construct your own temporary when default- or even value-initialisation is all you need. It isn't a fundamental change to how containers work, which is that they always hold valid instances of types.

Valid but in an unspecified state if you move from them!

查看更多
步步皆殇っ
6楼-- · 2019-01-02 21:50

Uninitialized values

You may have initialized value by creating the appropriate class. As the following:

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

    operator int () const { return i; }

private:
    int i;
};

The output is identical to "boost w/default".

Or create a custom allocator with construct and destroy as nop.

Splitting resize prototype

If void std::vector<T>::resize(size_type n) does what void bc::vector<T>::resize(size_type n, default_init_t) does, then lot of old valid code would break...


The splits of resize() allows to resize vector of 'move only' classes as the following:

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;
};
查看更多
笑指拈花
7楼-- · 2019-01-02 21:50

if you want to use a vector with the standard allocator, doesn't this work in 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));
   };
查看更多
登录 后发表回答