2

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.

18
  • There should be only 5 objects after the construction of the vector. Add a counter decrement operation to the destructor, and you should get the counting right. Commented Oct 25, 2015 at 22:18
  • @DanielStrul I'm interested in the total number of objects constructed over time. Commented Oct 25, 2015 at 22:19
  • 1
    Here's a similar question: stackoverflow.com/questions/6488323/… with the unfortunate answer. Commented Oct 25, 2015 at 22:43
  • 1
    @AlanStokes I gave an example in the question. Big O can be misleading. If the average elements each time is 4, then this is 25% more peak objects allocated. If the objects are large, this could be enough to fill the CPU cache for example. I'm not saying it is usually a problem, but there are cases where it could be, and the standard library should be well optimized for those cases. Commented Oct 25, 2015 at 22:58
  • 2
    This idea about avoiding the temporary is all about an extreme case of requiring convenient notation for a very border-line marginal optimization. For the usual case of using that temporary to initialize a bunch of vector items, the temporary doesn't matter. Where you want to avoid it you can, by creating a vector with the requisite capacity, just not with the convenient notation. The standard library can't supply convenient notation for every unrealistic use case. It just supplies the basic functionality and support for common usage. Commented Oct 26, 2015 at 1:31

2 Answers 2

1

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.

Sign up to request clarification or add additional context in comments.

Comments

1

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:

  1. Creation of many small vectors created within multiple larger classes
  2. All objects created either with the default construct-copy policy or by calling an emplace wrapper function.

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

16 Comments

The one with emplace_back performs the most copies?
What is the type you used in the container?
@NeilKirk , I used your code , try it out yourself, and test a big size , and yes it is reserved before to the appropriate size, before the emplace_back loop.
You need to run it with an object that is expensive to construct to be representative of my use case.
@NeilKirk , I would say an object which is expensive to copy , but still one has to make a choice.
|

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.