Reputation: 21773
Given this type for testing:
struct Counter
{
static int count;
Counter(int)
{
count++;
}
Counter(const Counter&)
{
count++;
}
Counter(Counter&&) noexcept
{
}
};
int Counter::count = 0;
Suppose we have the following:
std::vector<Counter> vec(5, 0);
According to VS2015, 6 Counter
objects are created. I know that there are only 5 permanent objects. Why doesn't the compiler emplace the objects from the constructor parameters, or move the temporary object into the first position and then copy the rest from it?
Even if the initial size of the vector is set to 0, 1 object is still created.
std::vector<Counter> vec(0, 0);
This could be important if the size isn't known until run-time and often it is 0 (a no-op) and the type in the container is expensive to copy or construct.
It's often convenient to initialize vectors in one statement, especially if they are class members in the initialization list or constants. How can I do that as efficiently as the following code:
std::vector<Counter> vec;
vec.reserve(size);
for (size_t i = 0; i < size; i++)
{
vec.emplace_back(0);
}
Which constructs only as many contained objects as there are stored in the vector.
Upvotes: 2
Views: 1900
Reputation: 3135
I guess it is efficient, based on your definition.
Testing the different versions;
Copies: 100000005 , Construct: 1, Equal copies 0
real 0m0.075s
user 0m0.073s
sys 0m0.002s
and with emplace_back:
Copies: 0 , Construct: 100000005, Equal copies 0
real 0m0.195s
user 0m0.191s
sys 0m0.004s
you may mean it is space efficient. However, that is a choice based on use case, and seems compiler designers prefer speed.
Here is the code ( I track also equal )
struct Counter
{
static int count_ctor;
static int count_copy;
static int count_equal;
Counter(int){count_ctor++;}
Counter(const Counter&){count_copy++;}
Counter(Counter&&) noexcept{}
Counter & operator=(Counter const &){ count_equal++ ;}
};
int Counter::count_copy = 0;
int Counter::count_ctor = 0;
int Counter::count_equal = 0;
int main(void)
{
int size(100000005);
#ifdef EMPLACE
std::vector<Counter> v;
v.reserve(size);
for(int i = size; i>0 ; --i){ v.emplace_back(0);}
#else
std::vector<Counter> v(size,0);
#endif
std::printf("Copies: %d , Construct: %d, Equal copies %d",Counter::count_copy, Counter::count_ctor, Counter::count_equal);
return 0;
}
compile with g++ -DEMPLACE --std=c++11 -O3 or without EMPLACE to get the desired binary.
SECOND TEST
In order to rebute the assumptions made by the OP , the following test has been made:
We produced two binaries with
g++ -DEMPLACE --std=c++11 -O3 copyc.cpp -o copyc && g++ --std=c++11 -O3 copyc.cpp -o copyc_copy
and in order to avoid any of the two having preferential treatement from the OS , we set a standard pause of 10s in between them, and we launch with the system at idle.
An exemplary run is below.
export K=10192 ; time ./copyc_copy $K ; sleep 10; time ./copyc $K
Copies: 10192 , Construct: 1, Equal copies 0
real 0m2.888s
user 0m0.666s
sys 0m2.219s
Copies: 0 , Construct: 10192, Equal copies 0
real 0m3.376s
user 0m1.105s
sys 0m2.270s
I've run this in multiple cases , and also in reverse
Copies: 0 , Construct: 10192, Equal copies 0
real 0m3.154s
user 0m0.886s
sys 0m2.267s
Copies: 10192 , Construct: 1, Equal copies 0
real 0m2.573s
user 0m0.531s
sys 0m2.025s
That being said this is an incoclusive test, but having spent this match time on this, I bet the compiler designers did more , and all from gnu to clang and VS decided to implement a construct-copy policy. I am certain they had other reasons as well.
The code for the second test is below:
#include <vector>
#include <iostream>
#include <cstdlib>
template<typename T> static std::vector<T> get5()
{
std::vector<T> s;
s.reserve(5);
for(int i=5; i!=0 ;--i)
{
s.emplace_back(T());
}
return s;
}
struct test_struct
{
volatile int internals[255];
};
struct test_create
{
std::vector<test_struct> s;
test_create() : s(5){}
};
struct test_emplace
{
std::vector<test_struct> s;
test_emplace() : s(get5<test_struct>()){}
};
struct Counter
{
static int count_ctor;
static int count_copy;
static int count_equal;
#ifdef EMPLACE
test_emplace t[100];
#else
test_create t[100];
#endif
Counter(int)
{
count_ctor++;
}
Counter(const Counter&)
{
count_copy++;
}
Counter(Counter&&) noexcept
{
}
Counter & operator=(Counter const &){ count_equal++ ;}
};
int Counter::count_copy = 0;
int Counter::count_ctor = 0;
int Counter::count_equal = 0;
int main(int arg, char const * argv[])
{
int size(std::atoi(argv[1]));
#ifdef EMPLACE
std::vector<Counter> v;
v.reserve(size);
for(int i = size; i>0 ; --i)
{
v.emplace_back(0);
}
#else
std::vector<Counter> v(size,0);
#endif
std::printf("Copies: %d , Construct: %d, Equal copies %d",Counter::count_copy, Counter::count_ctor, Counter::count_equal);
return 0;
}
Upvotes: 1
Reputation: 145279
You can simply define a function that creates a vector the way that you want.
As a function the initialization code is exception safe.
#include <iostream>
#include <vector>
#include <stddef.h> // ptrdiff_t
#include <utility> // std::forward
using namespace std;
struct Counter
{
static int n_constructor_calls;
Counter( int )
{
++n_constructor_calls;
}
Counter( Counter const& )
{
++n_constructor_calls;
}
Counter( Counter&& ) noexcept
{
++n_constructor_calls;
}
};
int Counter::n_constructor_calls = 0;
//--------------------------------------
using Size = ptrdiff_t;
using Index = Size;
template< class Item, class... Args >
auto make_vector( Size const n, Args&&... args )
-> vector<Item>
{
vector<Item> result;
result.reserve( n );
for( Index i = 0; i < n; ++i )
{
result.emplace_back( forward<Args>( args )... );
}
return result;
}
auto main() -> int
{
auto vec = make_vector<Counter>( 5, 42 );
cout << Counter::n_constructor_calls << " constructor calls.\n";
}
(This outputs “5 constructor calls.”)
You essentially ask, why isn't the vector
constructor defined to do this,
” Why doesn't the compiler emplace the objects from the constructor parameters, or move the temporary object into the first position and then copy the rest from it?
One reason is that this constructor was defined before move semantics was introduced in C++11.
Introducing additional constructors (which changes overload behavior), or changing the behavior of an existing constructor, is expensive with respect to the existing C++ code base, which is very large.
Upvotes: 1