in-place vector construction from initialization list (for class with constructor arguments)

11,182

Solution 1

List-initializing std::vector in your snippet is no different from doing the following (if initializer_list had a public non-explicit constructor or std::vector accepted an array reference.):

// directly construct with the backing array of 'initializer_list'
std::vector<A2> v(alias<A2[]>{ A2(2,3), A2(4,5), A2(8,9) });

It's not a special way to construct a std::vector that could take advantage of the implementation, really. List-initialization is a general-purpose way of "uniformly" initializing types. As such, there's no way it could tread std::vector any different than any other user-defined type. As such, having the construct in the OP do emplace construction is out of question.

Now, the backing array (or any constant array) may be put in read-only memory by the implementation, that's the reason why

std::initializer_list<T>::iterator

is just

typedef T const* iterator;

So moving out of std::initializer_list is also out of question.

Now, is there a solution? Yes, there is, and it's a rather easy one, actually!

We will want to have a free function that takes a container and a number of tuples equal to the number of elements you want to emplace. The tuples container the arguments to the constructor of the container type. Easy in theory, easy in practice with the indices trick (where indices == seq and build_indices == gen_seq in the code):

#include <type_traits>
#include <tuple>
#include <utility>

template<class T> using alias = T;
template<class T> using RemoveRef = typename std::remove_reference<T>::type;

template<class C, unsigned... Is, class Tuple>
void emplace_back_one(C& c, seq<Is...>, Tuple&& ts){
  c.emplace_back(std::get<Is>(std::forward<Tuple>(ts))...);
}

template<class T> using Size = std::tuple_size<RemoveRef<T>>;

template<class C, class... Tuples>
void emplace_back(C& c, Tuples&&... ts){
  c.reserve(sizeof...(Tuples));
  alias<char[]>{(
    emplace_back_one(c, gen_seq<std::tuple_size<RemoveRef<Tuples>>::value>{}, std::forward<Tuples>(ts))
  , '0')...};
}

Live example with the implementation of seq and gen_seq.

The code above calls emplace_back_one exactly sizeof...(Tuples) times, passing one tuple at a time in the order that the were passed to emplace_back. This code is also sequenced left-to-right, meaning that the constructors get called in the same order that you passed the tuples for them. emplace_back_one then simply unpacks the tuple with the indices trick and passes the arguments to c.emplace_back.

Solution 2

Construction of an object via std::initializer_list is no different than constructing an object from any other object. The std::initializer_list is not a mystical, phantasmal construct; it is a living, breathing C++ object (albeit a temporary one). As such, it obeys all the rules of regular living, breathing C++ objects.

Aggregate initialization can effectively elide the copy/moves because it's aggregate initialization, a purely compile-time construct. std::vector is many things; an aggregate and a purely compile-time construct are not among them. So, in order for it to initialize itself from what it is given, it must execute actual C++ code, not compile-time stuff. It must iterate over each element of the initializer_list and either copy those values or move them. And the latter is not possible, since std::initializer_list doesn't provide non-const access to its members.

Initializer list initialization is meant to look like aggregate initialization, not perform like it. That's the cost of having a runtime, dynamic abstraction like std::vector.

Share:
11,182

Related videos on Youtube

Johan Lundberg
Author by

Johan Lundberg

Experimental Particle physicist, got my PhD while with the IceCube neutrino telescope (@uw_icecube), then research at the ATLAS particle detector at CERN and Stockholm University. Now at RaySearch laboratories. https://abstractions.se Feel free to contact me at: lundberj ѳ ġmaİl˻ĉøm or at slack cpp-lang

Updated on June 04, 2022

Comments

  • Johan Lundberg
    Johan Lundberg almost 2 years

    Possible Duplicate:
    Can I list-initialize a vector of move-only type?

    Edit 1: Please consider a re-open vote: My question emphasize in-place construction. Move construction is an alternative but not what this questions is about. Thanks for the answers!

    Edit 2: Since I can't answer this question (It got closed) I post my own suggestion here. The following is not as good as the answers I accepted, but may be useful for others. At least only the move constructor is called:

    std::vector<A2> vec;
    {
      std::array<A2,3> numbers{{{2,3},{5,6},{7,8}}};
      vec.reserve(numbers.size());
      for (auto &v: numbers) vec.emplace_back(std::move(v)) ;
    }
    

    Original post:

    When thinking about the answer to this question: Initialization of classes within an STL array of vectors I found that I could not find a way to get in-place construction of the vector from an initialization list. What am I missing?

    Now trying to be more clear, I would like this (perfectly correct) initialization

    std::vector<A2> k{{2,3},{4,5},{8,9}};
    

    to have an effect more similar to this:

      std::vector<A2> k2;
      k2.reserve(3);
      k2.emplace_back(2,3);
      k2.emplace_back(4,5);
      k2.emplace_back(8,9);
    

    However, in the first case the copy constructor is called for A2 on a temporary while inserting. Is there a way to avoid that? What does the standard say?

    I desperately tried

    std::vector<A2> k{{2,3},{4,5},std::move(A2{8,9})};
    

    but that generates an additional call to the move constructor, something I also did not expect. I just wanted to explicitly hint that A2 is a temporary, something I had thought was implied.

    Full example:

    #include <vector>
    #include <iostream>
    
    struct A2 {
      int mk;
      int mj;
      A2(int k,int j) : mk(k),mj(j) {
        std::cout << "     constr for "<<this<< ":"<< mk<<std::endl;
      }
      A2(const A2& a2) {
        mk=a2.mk;
        mj=a2.mj;    
        std::cout << "copy constr for "<<this<< ":" << mk<<std::endl;
      }
      A2(A2&& a2) noexcept  {
        mk=std::move(a2.mk);
        mj=std::move(a2.mj);
        std::cout << "move constr for "<<this<< ":"<< mk<<std::endl;
      }
    };
    
    struct Ano {
      Ano() {
        std::cout << "     constr for "<<this <<std::endl;
      }
      Ano(const Ano& ano) {
        std::cout << "copy constr for "<<this<<std::endl;
      }
      Ano(Ano&& ano) noexcept  {
        std::cout << "move constr for "<<this<<std::endl;
      }
    };
    
    
    int main (){
      // here both constructor and copy constructor is called:
      std::vector<A2> k{{2,3},{4,5},std::move(A2{8,9})};
    
      std::cout << "......"<<std::endl;
      std::vector<A2> k2;
      k2.reserve(3);
      // here (naturally) only constructor is called:
      k2.emplace_back(2,3);
      k2.emplace_back(4,5);
      k2.emplace_back(8,9);
    
      std::cout << "......"<<std::endl;  
      // here only constructor is called:
      std::vector<Ano> anos(3);
    
    }
    

    Output:

         constr for 0xbf9fdf18:2
         constr for 0xbf9fdf20:4
         constr for 0xbf9fdf0c:8
    move constr for 0xbf9fdf28:8
    copy constr for 0x90ed008:2
    copy constr for 0x90ed010:4
    copy constr for 0x90ed018:8
    ......
         constr for 0x90ed028:2
         constr for 0x90ed030:4
         constr for 0x90ed038:8
    ......
         constr for 0x90ed048
         constr for 0x90ed049
         constr for 0x90ed04a
    
    • Johan Lundberg
      Johan Lundberg over 11 years
      It's not a duplicate, I'm after in-place initialization. Move construction would be a second option. True, the top answer to that question helps in explaining what is going on. That does not mean that the questions are the same.
    • Nicol Bolas
      Nicol Bolas over 11 years
      @JohanLundberg: If you can't move-initialize a vector with an initializer list, you certainly can't emplace-initialize it with one.
    • Jesse Good
      Jesse Good over 11 years
      I think initializer_list and move semantics explains a little better about the possible rationale (although I find the current behavior unintuitive).
    • Johannes Schaub - litb
      Johannes Schaub - litb over 11 years
      why do you want to avoid move construction? the emplace solution too requires a move constructor or copy constructor (if the former is throwing) to be accessible, even if it is not called.
  • Xeo
    Xeo over 11 years
    Aggregate initialization is not a compile-time construct, really. std::array<std::vector<int>, 3> a{{ {1,2}, {3,4}, {5,6} }}; will for sure require runtime constructor calls and dynamic allocation.
  • Nicol Bolas
    Nicol Bolas over 11 years
    @Xeo: But only because there's a non-aggregate in the middle of it. It stops being aggregate initialization once you get into the vector.
  • Nicol Bolas
    Nicol Bolas over 11 years
    The whole point of the intializer list mechanism is that it's readable. If you're going to have this list of forward_as_tuple stuff, one forwarding call for each element, you may as well just have a series of emplace_back statements. At least that's shorter.
  • Johan Lundberg
    Johan Lundberg over 11 years
    @NicolBolas, yes but this writes those emplace_back calls automatically, compile time, so why not? In practice I would probably just iterate at run-time, assuming construction is significantly more expensive than the loop and argument passing.
  • Nicol Bolas
    Nicol Bolas over 11 years
    @JohanLundberg: "yes but this writes those emplace_back calls automatically, compile time, so why not?" Because it takes up more physical space in code than a series of v.emplace_back calls.
  • Xeo
    Xeo over 11 years
    @Nicol: Sure, the current form might be a bit verbose, but you can always tinker with Boost.Preprocessor and then write EMPLACE_BACK(v, (1,2)(3,4)(5,6)), which shouldn't be too hard to do actually. :) Also, in generic code, where you might already have those tuples, I think it's pretty nice to have. Also, if you don't have temporaries or want to specifically move stuff into the elements, you can use std::tie, which is a lot less verbose.
  • Richard Cook
    Richard Cook over 8 years
    @Xeo: Your link no longer works. Could you republish the full example?