9

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!

Nicol Bolas
  • 449,505
  • 63
  • 781
  • 982
Vladislav Ihost
  • 2,127
  • 11
  • 26
  • 2
    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. – molbdnilo Apr 03 '19 at 08:42
  • @molbdnilo Of course. Mentioned function `func` does return value, which is bound to const reference `c`. But copy-constructor is invoked, which is erroneous. – Vladislav Ihost Apr 03 '19 at 08:46
  • 1
    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. – molbdnilo Apr 03 '19 at 09:07
  • 1
    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 occurring – Caleth Apr 03 '19 at 09:07
  • 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. – n. m. could be an AI Apr 03 '19 at 09:37
  • @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 advance – Vladislav Ihost Apr 03 '19 at 09:39
  • @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. – Vladislav Ihost Apr 03 '19 at 09:40
  • 1
    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. – Cubbi Apr 03 '19 at 13:25
  • 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". – n. m. could be an AI Apr 03 '19 at 13:54
  • @VladislavIhost: "*In C++14 there was introduced copy elision and return value optimization.*" Incorrect; it has been part of C++ since C++98/03. – Nicol Bolas Apr 03 '19 at 14:26

2 Answers2

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

Jaa-c
  • 5,017
  • 4
  • 34
  • 64
  • 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. Thanks – Vladislav Ihost Apr 03 '19 at 09:37
  • 1
    @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. – Jaa-c Apr 03 '19 at 13:06
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).

MAChitgarha
  • 3,728
  • 2
  • 33
  • 40
luk32
  • 15,812
  • 38
  • 62