7

Consider below code:

#include <iostream>
#include <vector>

struct C {
  C() {}
  C(const C&) { std::cout << "A copy was made.\n"; }
  C(C&&) {std::cout << "A move was made.\n";}
};

std::vector<C> g() {
  std::vector<C> ret {C(), C(), C()};
  return ret;
}

std::vector<C> h() {
  std::vector<C> ret;
  ret.reserve(3);
  ret.push_back(C());
  ret.push_back(C());
  ret.push_back(C());
  return ret;
}

int main() {
  std::cout << "Test g\n";
  std::vector<C> v1 = g();

  std::cout << "Test h\n";
  std::vector<C> v2 = h();
}

Compiled with g++ -std=c++11 main.cpp && ./a.out, the result is:

Test g
A copy was made.
A copy was made.
A copy was made.
Test h
A move was made.
A move was made.
A move was made.

Note that both functions uses copy elision so the returned std::vector<C> is not copied.

I understand why h() uses move-constructor, but why g() uses copy-constructor?

From vector's doc:

(6) initializer list constructor

Constructs a container with a copy of each of the elements in il, in the same order.

It looks like initializer-list always copy the elements, then probably it mean the initializer-list constructor's performance could be impacted if C is cheap to move but heavy to copy.

So my question: what is the preferred way to initialize a container (e.g. vector) with objects that are cheap to move but heavy to copy?

Mine
  • 4,123
  • 1
  • 25
  • 46
  • 4
    `return std::vector(3);`. And find a way to make `C`'s move constructor `noexcept`. – ildjarn Jul 19 '16 at 08:02
  • 2
    Do you need to supply any arguments in your real code, or are you just using the default constructor? – TartanLlama Jul 19 '16 at 08:02
  • @ildjarn @TartanLlama I know `std::vector(3);` avoid any copy or move, but if I want to initialize with objects with parameters (not default constructor), I can not use `std::vector(3);`. So in that case, what's the preferred way? – Mine Jul 19 '16 at 08:11
  • 1
    In that case, I wouldn't do *either* of your methods; I'd `emplace` them after reserving the appropriate space. – WhozCraig Jul 19 '16 at 08:13

3 Answers3

3

You can move from an initializer list with a bit of boilerplate.

template<class T>
struct force_move{
  mutable T t;

  template<class...Args>
  force_move(Args&&...args):
    t(std::forward<Args>(args)...)
  {}
  // todo: code that smartly uses {} if () does not work?

  force_move()=default;
  force_move(force_move const&)=delete;

  template<class U, class...Args>
  force_move(std::initializer_list<U> il, Args&&...args):
    t(il, std::forward<Args>(args)...)
  {}

  operator T()const{ return std::move(t); }
};

template<class T>
struct make_container {
  std::initializer_list<force_move<T>> il;
  make_container( std::initializer_list<force_move<T>> l ):il(l) {}

  template<class C>
  operator C()&&{
    return {il.begin(), il.end()};
  }
};

Use:

std::vector<C> v=make_container<C>{ {}, {} };

This is concise, efficient, and solves your problem.

(Possibly it should be operator T&& above. Not sure, and I am leery of ever returning an rvalue reference...)

Now, this seems a bit of a hack. But, the alternatives suck.

The manual push back/emplace back list is ugly, and gets uglier after you add in reserve requirements for maximal efficiency. And the naive il solution cannot move.

Solutions that do not let you list the elements right there where the instance is declared are awkward, in my opinion. You want the ability to put list of contents adjacent to declaration.

Another "local list" alternative is to create a variardic function that internally initializes a std::array (possibly of ref wrappers) which then moves from that array into the container. This does not allow { {}, {}, {} } style lists, however, so I find it lacking.

We could do this:

template<class T, std::size_t N>
std::vector<T> move_from_array( T(&arr)[N] ){
  return {std::make_move_iterator(std::begin(arr)), std::make_move_iterator(std::end(arr))};
}

Then:

C arr[]={{}, {}, {}};
std::vector<C> v = move_from_array(arr);

the only downside is requiring two statements at point of use. But the code is less obtuse than my first solution.

Yakk - Adam Nevraumont
  • 262,606
  • 27
  • 330
  • 524
  • The `force_move` and `make_container` does work, but `std::vector v3 = make_container{C(), C(), C()};` costs 9 move constructor calls. So maybe `move_from_array` could do a better job? – Mine Jul 19 '16 at 12:52
  • @Mine Don't `C()`, just `{}`. That saves 3 moves: you created temporaries for no reason. Replace `operator C()` with `operator C&&()`. That saves 3 moves. – Yakk - Adam Nevraumont Jul 19 '16 at 13:00
  • Change to `{}` does saves 3 moves, but interestingly, `operator C()&&` on clang gives 3 moves, but gcc give 6 moves. – Mine Jul 19 '16 at 13:08
  • @Mine I said `operator C&&()`... And possible gcc is moving into a temporary. Add some no exception guarantees in the code may help. – Yakk - Adam Nevraumont Jul 19 '16 at 13:40
  • actually changing to `operator C&&()` causes compile warning and runtime coredump, see [example](http://coliru.stacked-crooked.com/a/180780d405371885). And I don't quite get how `operator C&&()` works. Could you kindly elaborate? – Mine Jul 19 '16 at 14:44
  • @mine laugh, I meant `operator T&&()const{ return std::move(t); }`. My bad, got letters mixed up. As noted, probably need to mark up methods as not throwing as well. – Yakk - Adam Nevraumont Jul 19 '16 at 15:01
3

You cannot move from an initializer_list (barring gymnastics with mutable as in Yakk's answer) because the elements of an initializer_list are declared const (moving elements of an initialization_list considered dangerous?).

I would recommend constructing your objects in a container with aggregate initialization i.e. a classic array or std::array, then constructing the vector from move iterators:

std::vector<C> h() {
    C[] arr{C(), C(), C()};
    return std::vector<C>(
        std::make_move_iterator(std::begin(arr)),
        std::make_move_iterator(std::end(arr)));
}
Community
  • 1
  • 1
ecatmur
  • 152,476
  • 27
  • 293
  • 366
  • Actually Yakk's answer appears to be ok. He is not moving from the `initializer_list`, but only converting the elements of it. Under the hood, the contents of the initializer_list are modified. But that is ok, since its elements cannot be put into read-only memory, because `mutable` prevents that. – Ralph Tandetzky Jul 19 '16 at 10:29
  • @RalphTandetzky sorry, I didn't mean to imply that Yakk's answer was wrong; just that it's quite complicated (though reusable) and that without it you would have illegal behavior. – ecatmur Jul 19 '16 at 13:05
2

Unfortunately std::initializer_list doesn't really work with move semantics.

If you need to supply arguments to the constructor, I would instead use emplace_back, which constructs the element in place:

std::vector<C> h() {
  std::vector<C> ret;
  ret.reserve(3);
  ret.emplace_back(arg1);
  ret.emplace_back(arg2,arg3);
  ret.emplace_back(0,12);
  return ret;
}
Community
  • 1
  • 1
TartanLlama
  • 63,752
  • 13
  • 157
  • 193
  • 1
    don't forget constructors called with `emplace_back` have to be `noexcept`. Otherwise, default+ copy constructors will be called instead. – Andrei R. Jul 19 '16 at 08:33
  • @AndreiR.: Cannot reproduce your extra copy: [Demo](http://coliru.stacked-crooked.com/a/3074b357819752ac) – Jarod42 Jul 19 '16 at 09:37
  • @Jarod42, this is because `reserve` also affects behavior: [demo](http://coliru.stacked-crooked.com/a/32d2cc583df98abe). In general, move constructors/assignment operators have to be noexcept to work with many algorithms (`std::move_if_noexcept` is widely used in stl) – Andrei R. Jul 19 '16 at 09:54