in-place vector construction from initialization l

2019-02-12 23:25发布

问题:

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

回答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.



回答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.