19

I see a lot of code at work where people use emplace and emplace_back with a temporary object, like this:

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

vector<A> v;
vector<A>.emplace_back(A(1, 2));

I know that the whole point of emplace_back is to be able to pass the parameters directly, like this:

v.emplace_back(1, 2);

But unfortunately this is not clear to a few people. But let's not dwell on that....

My question is: is the compiler able to optimize this and skip the create and copy? Or should I really try to fix these occurrences?

For your reference... we're working with C++14.

Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
mystery_doctor
  • 404
  • 2
  • 11

3 Answers3

13

My question is: is the compiler able to optimize this and skip the create and copy? Or should I really try to fix these occurrences?

It can't avoid a copy, in the general case. Since emplace_back accepts by forwarding references, it must create temporaries from a pure standardese perspective. Those references must bind to objects, after all.

Copy elision is a set of rules that allows a copy(or move) constructor to be avoided, and a copy elided, even if the constructor and corresponding destructor have side-effects. It applies in only specific circumstances. And passing arguments by reference is not one of those. So for non-trivial types, where the object copies can't be inlined by the as-if rule, the compiler's hands are bound if it aims to be standard conformant.

StoryTeller - Unslander Monica
  • 165,132
  • 21
  • 377
  • 458
  • 1
    I see. So definitely no copy elision, but could there be a move if the object supported it? – mystery_doctor Jun 07 '18 at 12:19
  • 2
    @mystery_doctor - For sure since it's a temporary bound to a forwarding reference (which will preserve value category). It's worth mentioning that copy elision applies to both copy and move c'tor calls, despite the name. A move of a return value can also be elided entirely. – StoryTeller - Unslander Monica Jun 07 '18 at 12:28
5

The easy answer is no; elision doesn't work with perfect forwarding. But this is so the answer is actually yes.

It requires a touch of boilerplate:

struct A {
  A(int, int){std::cout << "A(int,int)\n"; }
  A(A&&){std::cout<<"A(A&&)\n";}
};

template<class F>
struct maker_t {
  F f;
  template<class T>
  operator T()&&{ return f(); }
};

template<class F>
maker_t<std::decay_t<F>> maker( F&& f ) { return {std::forward<F>(f)}; }

vector<A> v;
v.emplace_back(maker([]{ return A(1,2); }));

live example.

Output is one call to A(int,int). No move occurs. In the making doesn't even require that a move constructor exist (but the vector does, as it thinks it may have to move the elements in an already allocated buffer). In the moves are simply elided.

Yakk - Adam Nevraumont
  • 262,606
  • 27
  • 330
  • 524
  • Isn't this dependent on return value optimisation? It doesn't look like it would apply to the OP's `emplace_back(A(1,2));` - you're skipping the move because the construction is in a return statement, rather than a parameter to a function. – Dave Jun 07 '18 at 18:13
  • 1
    @Dave RVO is one kind of elision in [tag:C++14]. in [tag:C++17] the "optimization" is a requirement. What I did was turn code that creates an `A` into code that permits emplacing of that `A` directly without a move operation within a container. – Yakk - Adam Nevraumont Jun 07 '18 at 18:35
4

is the compiler able to optimize this and skip the create and copy?

There is not necessarily a copy involved. If a move constructor is available, there will be a move. This cannot be optimized away, as the direct initialization case will just call the init constructor, while in the other case, the move constructor will be called additionally (including its side-effects).

Therefore, if possible, you should refactor that code.

Catskul
  • 17,916
  • 15
  • 84
  • 113
Jodocus
  • 7,493
  • 1
  • 29
  • 45