3

I have a struct, say

struct A { A(int n) : n(n) {} int n; }; 

and I want to initialize a std::vector with some elements. I can do this by using an initialization list, or by emplacing the new elements:

// 1st version: 3 ctors, 3 copy ctors, 3 dtors std::vector<A> v1{1, 2, 3}; // 2nd version: 3 ctors std::vector<A> v2; v2.reserve(3); v2.emplace_back(4); v2.emplace_back(5); v2.emplace_back(6); 

As the comments show, the first version calls 3 constructors, 3 copy constructors, and 3 destructors. The version with emplace only uses 3 constructors.

Question: Obviously the second version is better, but the first version is more succinct. Can I have the best of both worlds? Can I do a direct initialization without the extra cost?

(Here's a longer version of the A struct that shows what's happening.)

9
  • 2
    Your version with emplace_back-s has no extra cost in terms of calling methods of A. Since you want to have vector of size = 3 at the end, it is absolutely necessary to call 3 constructors. emplace_back perfectly forward your arguments into constructors of elements. Commented Jul 30, 2015 at 18:06
  • (I deleted a comment I made a moment ago - I was wrong. I had forgotten about the role of the initializer_list on this.) Commented Jul 30, 2015 at 18:11
  • 1
    @vukung: In the case of the brace-intialization, the compiler creates 3 temporary A objects to populate the std::initializer_list that it passes to the std::vector constructor, then those values are copied into the A objects that are allocated within the std::vector, and then finally the temporaries are destructed after the std::vector constructor exits. Commented Jul 30, 2015 at 18:32
  • 1
    @vukung, to clarify. You want the efficiency of emplace_back, but without having to type out emplace_back three times? Which would be annoying if you had much more than three to type. Also, direct initialization allows the vector to know the size in advance and allocate accordingly Commented Jul 30, 2015 at 18:46
  • 2
    @vukung, I never really look at the implementation of init lists before, but now I've looked into them in more detail I am not entirely impressed with them. Their begin method returns a const pointer, meaning that they can't be moved from. I had assumed that init lists were designed to be moved from! There are many things I would change about them actually Commented Jul 30, 2015 at 19:10

2 Answers 2

5

Since A is convertible from int, you can use the range constructor of vector:

auto inits = {1, 2, 3}; std::vector<A> v1{std::begin(inits), std::end(inits)}; 

Or in a single declaration-statement (assuming you can rely on RVO):

auto v1 = [inits={1, 2, 3}] { return std::vector<A>{std::begin(inits), std::end(inits)}; }(); 
Sign up to request clarification or add additional context in comments.

2 Comments

Nice! So if I use a range constructor, there is no copying involved. Strange that it's not the default behavior for the direct initialization...
@vukung it's because vector only has an initializer_list constructor for its element type, and initializer_lists are only really efficient for literal types. A converting initializer_list<U> constructor would only be useful for types T convertible from U, so I guess they didn't bother.
2

Expanding on @ecatmur's answer, I've developed a piece of code that allows a very general solution for any type of vector and for any constructor calls. The constructor parameters for each element of the vector are stored in a tuple (of & or && as appropriate) which are then perfect-forwarded when the element is built. Each element is constructed only once, essentially equivalent to emplace_back. This forwarding would even allow a vector of move-only types to be built such as unique_ptr<?>.

(Update, due to RVO it should simply construct them in place. Unfortunately, however, the element type does require at least a copy-constructor or move-constructor to be visible, even if they are actually skipped by the optimizer. This means you can build a vector of unique_ptr, but not of mutex.)

auto v2 = make_vector_efficiently<A>( pack_for_later(1) // 1-arg constructor of A ,pack_for_later(2,"two") // 2-arg constructor of A ,pack_for_later(3) // 1-arg constructor of A ); 

The above code will create a vector<A> with three elements. In my example, A has two constructors, one which takes int,string as parameters.

pack_for_later builds a tuple that stores its parameters as &/&& references. That is then converted into on object (of type UniquePointerThatConverts, that has the desired conversion operator, in this case operator A().

Within make_vector_efficiently, an initializer list of these converter objects is built and then vector is constructed with the begin() and end() of the initializer_list. You might expect that these iterators would be required to have type T* in order to construct a vector<T>, but it is sufficient that the type the iterator points to can convert to T.

The constructor then uses placement new to (copy-)construct from the converted object. But, thanks for RVO, the copy won't happen and the converter will be effectively doing the equivalent of emplace_back for us.

Anyway, any feedback appreciated. Finally, it's trivial to extend this to other containers besides vector.


Full code on Coliru Should work on any C++11 compiler.


Some more detailed notes, and copies of the important functions:

pack_for_later simply builds a std::tuple. The standard make_tuple isn't good enough as it ignores references. Each element of the tuple built by pack_for_later is a reference (& or && as appropriate, depending on whether the original parameter was an lvalue or rvalue)

template<typename ...T> std:: tuple<T&&...> pack_for_later(T&&... args) { // this function is really just a more // 'honest' make_tuple - i.e. without any decay return std:: tuple<T&&...> (std::forward<T>(args)...); } 

Next, make_vector_efficiently is the function that brings is all together. It's first parameter is the 'Target' type, the type of the elements in the vector we wish to create. The collection of tuples is converted into our special converter type UniquePointerThatConverts<Target> and the vector is constructed as discussed above.

template<typename Target, typename ...PackOfTuples> auto make_vector_efficiently(PackOfTuples&&... args) -> std::vector<Target> { auto inits = { UniquePointerThatConverts<Target>(std::forward<PackOfTuples>(args))...}; return std::vector<Target> {std::begin(inits), std::end(inits)}; } 

Because A can have multiple constructors, and we want to be able to use any and all of them, pack_for_later can return many different types (don't forget about lvalues and rvalues too). But we need a single type to build the init list from. Therefore, we define a suitable interface:

template<typename Target> struct ConvInterface { virtual Target convert_to_target_type() const = 0; virtual ~ConvInterface() {} }; 

Each tuple is therefore converted to an object that implements this interface by make_Conv_from_tuple. It actually returns a unique_ptr to such an object which is then stored in a UniquePointerThatConverts that has the actual conversion operator. It is this type that is stored in the init list which is used to initialize the vector.

template<typename Target> struct UniquePointerThatConverts { std:: unique_ptr<ConvInterface<Target>> p; // A pointer to an object // that implements the desired interface, i.e. // something that can convert to the desired // type (Target). template<typename Tuple> UniquePointerThatConverts(Tuple&& p_) : p ( make_Conv_from_tuple<Target>(std:: move(p_)) ) { //cout << __PRETTY_FUNCTION__ << endl; } operator Target () const { return p->convert_to_target_type(); } }; 

And, of course, the actual conversion operator which constructs from the pack.

template<typename Target, typename ...T> struct Conv : public ConvInterface<Target> { std::tuple<T...> the_pack_of_constructor_args; Conv(std::tuple<T...> &&t) : the_pack_of_constructor_args(std:: move(t)) {} Target convert_to_target_type () const override { using idx = typename make_my_index_sequence<sizeof...(T)> :: type; return foo(idx{}); } template<size_t ...i> Target foo(my_index_sequence<i...>) const { // This next line is the main line, constructs // something of the Target type (see the return // type here) by expanding the tuple. return { std:: forward < typename std:: tuple_element < i , std::tuple<T...> > :: type > (std:: get<i>(the_pack_of_constructor_args)) ... }; } }; 

3 Comments

Great work! I still have to wade through the details, but looks like a general solution.
I made a little tweak to the code on Coliru, and updated the link here. A specialization for my_index_sequence<0> was missing, and this is needed to call default constructors (i.e. in order that pack_for_later() works). Also, I've discovered that this won't work for vector<mutex> because RVO doesn't allow you to skip the need for a copy-constructor or move-constructor to be present (even though they'll never be called under RVO).
Nice! I'm a bit baffled by how map has emplace(piecewise_construct, [tuples]) but vector et al do not. Perhaps - and especially if this is easily generalised as you said - you could submit a library proposal? It seems like the kind of thing that should just exist out-of-the-box - and indeed, does elsewhere. I also wonder whether C++17's guaranteed copy elision could help you out with the requirement for visible copy/move ops? Lastly, is it me, or does your example create a vector with 3 elements, not the claimed 4? :S

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.