Reputation: 11696
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.
Upvotes: 44
Views: 17679
Reputation: 144
A small note regarding Caseys answer:
As Casey notes under 1., above code only interposes on value-initialization. I don't know whether this entails, the following - but at least for me, it was not obvious.
Above code only avoids initialization of Plain Old Datatype [POD]-ed T
in std::vector<T, default_init_allocator<T>>
.
Whether this actually avoids runtime overhead however currently I don't know. Maybe someone else can answer that for me.
In case anyone wants to test this, I will append the code I used for actually testing this below. I should probably add that I used set(CMAKE_CXX_STANDARD 11)
and a MinGW 8.1.0 64-bit C++ compiler.
// ----------------------------------------------------
#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);
}
}
// ----------------------------------------------------
The output I got from this was:
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: This full-fledged answer only came to be, because I am as of yet not allowed to make comments. Otherwise I probably would have only asked Casey to extend the answer above with a note, that this essentially only works for PODs.
Upvotes: 1
Reputation: 42554
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
// 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)...);
}
};
Upvotes: 88
Reputation: 11
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));
};
Upvotes: 1
Reputation: 217135
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.
resize
prototypeIf 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;
};
Upvotes: 3
Reputation: 218710
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.
Upvotes: 32
Reputation: 308121
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.
Upvotes: 2
Reputation: 385106
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!
Upvotes: 2