Disable copy elision in C++
Asked Answered
P

2

10

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://mcmap.net/q/1166154/-disable-return-value-optimization-for-one-function . 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://mcmap.net/q/1166155/-how-can-i-disable-c-return-value-optimization-for-one-type-only . 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!

Playreader answered 3/4, 2019 at 8:25 Comment(10)
Returning a reference to a temporary does not extend the temporary's lifetime. Only returned values get their lifetimes extended, if bound to a const reference.Kristynkrock
@Kristynkrock Of course. Mentioned function func does return value, which is bound to const reference c. But copy-constructor is invoked, which is erroneous.Playreader
The problem isn't func, but noelide. clang says "error: C-style cast from rvalue to reference type 'Clazz (&)[1]'", and g++ something similar. VC++ is notoriously unreliable.Kristynkrock
The C++14 standard explicitly allows copy elision beyond the "As-If" rule, and the C++17 standard changes when an object is defined to exist, such that "copy elision" is just following the rules of temporary materialisation. In a very real sense, there isn't a copy occurringPentheam
It is not possible to "disable copy elision". It is a part of the language. You may want to formulate your requirements in terms of observable effects.Gaslight
@n.m. There should be following observable effects in example above: copy constructor should be invoked for a and b variables, and should not be invoked to c variable. Thanks in advancePlayreader
@n.m. In other words, observable effect should be LIKE (not mandatory code struct equal) #pragma GCC optimize ("no-elide-constructors") applied, but cross-compiler.Playreader
What does C++14 have to do with it? Copy elision and return value optimization were standardized in C++98 based on widespread common practice.Isomerous
The standard defines exactly what observable behaviour is, but for our purposes the following could be sufficient. "Program calls function F and outputs A, I want a method to modify F such that the program outputs B".Gaslight
@VladislavIhost: "In C++14 there was introduced copy elision and return value optimization." Incorrect; it has been part of C++ since C++98/03.Kare
L
5

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&&>(...).

Louislouisa answered 3/4, 2019 at 9:13 Comment(3)
Brilliant! Solution with std::move works good in MSVC and even GCC. Looks like there should be two template functions - like noelide and noelide_ref. Second should not materialize temporary. ThanksPlayreader
@VladislavIhost: I'm not sure I understand your comment, I think you should be ok with just one implementation of noelide, that would return T&&... Basically exactly what std::move does.Louislouisa
can this be used to prevent this syntax auto r = f()? for when f returns the non copyable type?Davenport
F
4

This is impossible to be done given all your restrictions. Simply, because the standard does not provide a way of turning off RVO optimizations.

You can prevent mandatory application of RVO by breaking one of the requirements, but you cannot reliably prevent optional allowed optimization. Everything you do is either changing semantics or compiler specific at this point (e.g. -fno-elide-constructors option for GCC and Clang).

Febri answered 3/4, 2019 at 8:47 Comment(1)
fno-elide-constructors has no effect in c++17.Davenport

© 2022 - 2024 — McMap. All rights reserved.