Disclaimer: Goal of research is how to disable copy elision and return value optimization for supplied part of code. Please avoid from answering if want to mention something like XY-problem. The question has strictly technical and research character and is formulated strongly in this way
In C++14 there was introduced copy elision and return value optimization. If some object had been destructed and copy-constructed in one expression, like copy-assignment or return immediate value from function by value, copy-constructor is elided.
Following reasoning is applied to copy constructor, but similar reasoning can be performed for move constructor, so this is not considered further.
There are some partial solutions for disabling copy elision for custom code:
1) Compiler-dependent option. For GCC, there is solution based on __attribule__
or #pragma GCC
constructions, like this https://stackoverflow.com/a/33475393/7878274 . But since it compiler-dependent, it does not met question.
2) Force-disabling copy-constructor, like Clazz(const Clazz&) = delete
. Or declare copy-constructor as explicit
to prevent it's using. Such solution does not met task since it changes copy-semantics and forces introducing custom-name functions like Class::copy(const Clazz&)
.
3) Using intermediate type, like describe here https://stackoverflow.com/a/16238053/7878274 . Since this solution forces to introduce new descendant type, it does not met question.
After some research there was found that reviving temporary value can solve question. If reinterpret source class as reference to one-element array with this class and extract first element, then copy elision will turned off. Template function can be written like this:
template<typename T, typename ... Args> T noelide(Args ... args) {
return (((T(&)[1])(T(args...)))[0]);
}
Such solution works good in most cases. In following code it generates three copy-constructor invocations - one for direct copy-assignment and two for assignment with return from function. It works good in MSVC 2017
#include <iostream>
class Clazz {
public: int q;
Clazz(int q) : q(q) { std::cout << "Default constructor " << q << std::endl; }
Clazz(const Clazz& cl) : q(cl.q) { std::cout << "Copy constructor " << q << std::endl; }
~Clazz() { std::cout << "Destructor " << q << std::endl; }
};
template<typename T, typename ... Args> T noelide(Args ... args) {
return (((T(&)[1])(T(args...)))[0]);
}
Clazz func(int q) {
return noelide<Clazz>(q);
}
int main() {
Clazz a = noelide<Clazz>(10);
Clazz b = func(20);
const Clazz& c = func(30);
return 0;
}
This approach works good for a
and b
cases, but performs redundant copy with case c
- instead of copy, reference to temporary should be returned with lifetime expansion.
Question: how to modify noelide
template to allow it work fine with const lvalue-reference with lifetime expansion?
Thanks!
GCC provides the -fno-elide-constructors option to disable copy-elision. This option is useful to observe (or not observe) the effects of return value optimization or other optimizations where copies are elided. It is generally not recommended to disable this important optimization.
Named RVO GCC performs NRVO by default, but it can be disabled using the -fno-elide-constructors + compiler option. In contrast, MSVC disables NRVO by default, but it can be enabled using /O2 optimization+.
Guaranteed copy elision redefines a number of C++ concepts, such that certain circumstances where copies/moves could be elided don't actually provoke a copy/move at all. The compiler isn't eliding a copy; the standard says that no such copying could ever happen.
According to N4140, 12.8.31:
...
This elision of copy/move operations, called copy elision, is permitted in the following circumstances (which may be combined to eliminate multiple copies):
(31.1) — in a return statement in a function with a class return type, when the expression is the name of a non-volatile automatic object (other than a function or catch-clause parameter) with the same cv-unqualified type as the function return type, the copy/move operation can be omitted by constructing the automatic object directly into the function’s return value
(31.3) — when a temporary class object that has not been bound to a reference (12.2) would be copied/moved to a class object with the same cv-unqualified type, the copy/move operation can be omitted by constructing the temporary object directly into the target of the omitted copy/move
So if I understand it correctly, copy elision can only occur, if the return statement is a name of a local variable. So you can for example 'disable' copy elision by returning e.g. return std::move(value)
... If you don't like using move
for this, you can simply implement noelide
as a static_cast<T&&>(...)
.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With