Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

What is the preferred way to initialize a container with objects that are cheap to move but heavy to copy

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?

like image 365
Mine Avatar asked Jul 19 '16 07:07

Mine


2 Answers

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.

like image 183
Yakk - Adam Nevraumont Avatar answered Oct 07 '22 01:10

Yakk - Adam Nevraumont


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)));
}
like image 42
ecatmur Avatar answered Oct 07 '22 00:10

ecatmur