c++gcccopy-elisionemplacestdoptional

Copy/move elision vs emplace with std::optional


I was using an std::optional to store a struct, and I initially went with:

std::optional<Point_t> optPosition; // declared somewhere as a class member
optPosition.emplace(..., ..., ...);

works great, but I don't really like that you have to define the constructor explicitly in the Point_t struct, and also it's not super readable (imagine if I had more members).

So I tried some alternatives:

optPosition.emplace(point_t {
  .x = ...,
  .y = ...,
  .z = ...
});

and

optPosition = point_t {
  .x = ...,
  .y = ...,
  .z = ...
};

I was afraid that I would be hit by some copy/move overhead, but GCC eludes them, and the generated assembly is the same in all 3 cases (GCC11).

My question is:
Is this guaranteed by the standard, or is it GCC being nice ?
If guaranteed, then in what case is emplace useful ?
In general, besides checking the asm code, how can I know that no copy/move will be added, and when should I prefer the first case (the not-very-readable-emplace) ?


Solution

  • std::optional<T>::emplace(x, y, z) guarantees that it forwards x, y, z to the constructor of T and does not make any copies or moves of the constructed T object.

    std::optional<T>::emplace({...}) where {...} is a designated-initializer-list that can convert to T will not compile, so I'm not sure what you mean when you claim that it results in the same assembly. See Godbolt. In any case, designated-initializer-lists cannot be perfectly forwarded so even if this worked, you could not expect elision to be possible.

    In your other alternative, you are passing a point_t object to the operator= function, which will take its argument by reference (see constructor 4 on cppreference). Binding a reference forces the materialization of a temporary of point_t, which then has to be moved into the stored T object. A temporary that is bound to a reference cannot be elided. Furthermore, if a temporary T object were not created, there would be no RHS for the stored T object to be assigned from.

    However, when we say elision cannot occur, it simply means that, according to the standard, the program must behave as though the temporary object is created and then copied/moved. If T is trivially copyable (which I'm guessing your Point_t is) then it is impossible to observe whether there were any temporaries that were copied/moved into the stored T object. Whenever the effects of an optimization are unobservable, the compiler is always allowed to perform such an optimization. But, of course, it cannot be guaranteed by the standard. Equally, even if the standard guarantees no copies/moves, you have no guarantee that a crappy compiler will not generate wildly inefficient assembly code that shuffles values pointlessly between various registers before writing them into the T object.