25

Given

struct Range{
    Range(double from, double to) : from(from), to(to) {}
    double from;
    double to;
};

struct Box{
    Box(Range x, Range y) : x(x), y(y) {}
    Range x;
    Range y;
};

suppose we run Box box(Range(0.0,1.0),Range(0.0,2.0)).

Could a modern compiler with optimizations enabled avoid copying Range objects altogether during this construction? (i.e. construct the Range objects inside box to begin with?)

Museful
  • 6,711
  • 5
  • 42
  • 68

4 Answers4

30

There are actually two copies being performed on each Range object passed to the constructor. The first happens when copying the temporary Range object into the function parameter. This can be elided as per the reference given in 101010's answer. There are specific circumstances in which copy elision can be performed.

The second copy happens when copying the function parameter into the member (as specified in the constructor initialization list). This cannot be elided, and this is why you still see a single copy being made for each parameter in YSC's answer.

When the copy constructor has side-effects (such as the prints in YSC's answer), copy elision can still be performed for the first copy, but the second copy must remain.

However, the compiler is always free to make changes if they do not alter the observed behavior of the program (this is known as the "as-if" rule). This means that if the copy constructor has no side effects and removing the constructor call will not change the result, the compiler is free to remove even the second copy.

You can see this by analyzing the generated assembly. In this example, the compiler optimizes out not only the copies, but even the construction of the Box object itself:

Box box(Range(a,b),Range(c,d));
std::cout << box.x.from;

Generates identical assembly as:

std::cout << a;
Cœur
  • 37,241
  • 25
  • 195
  • 267
interjay
  • 107,303
  • 21
  • 270
  • 254
  • 2
    Maybe this could be improved if you cited more clearly that there are two things at play here (1) Copy Elision, which allows eliding copies no matter how the copy constructor/destructor are implemented, is a C++ specific rule and (2) the As If rule, which allows eliding any code that does not change the behavior, is a generic optimizer rule. The other answers failed at (2) because they started instrumenting the copy constructor/destructor calls to witness the copies which in turn inhibited the "As If" rule and thus had the optimizers retain the copy behavior. – Matthieu M. Nov 23 '15 at 14:53
  • @MatthieuM. I've edited to hopefully make the distinction clearer. – interjay Nov 23 '15 at 15:07
  • "When the copy constructor has side-effects (such as the prints in YSC's answer), copy elision can still be performed for the first copy, but the second copy must remain." Do you have a source for this? I'd be interested to learn more about it. – Jordan Melo Nov 26 '15 at 14:25
  • 2
    @JordanMelo If you're asking why the first copy can be elided, it's because the standard allows it in its rules for [copy elision](http://en.cppreference.com/w/cpp/language/copy_elision), which is applicable even if the constructor has side effects. The relevant rule here is eliding a copy from a temporary. If you're asking why the second must remain, it's because it doesn't fall into the allowed applications for copy elision, and due to its having side-effects it can't be removed by the [as-if rule](http://en.cppreference.com/w/cpp/language/as_if). – interjay Nov 26 '15 at 14:56
  • 1
    Why doesn't the second case fall into the allowed applications for copy elision? Is it because it's not a copy from a temporary? – Jordan Melo Nov 26 '15 at 15:00
  • 2
    @JordanMelo Correct. The constructor parameters `from` and `to` are named variables and therefore not temporaries. – interjay Nov 26 '15 at 15:08
  • @MatthieuM but copy elision is the one case where the compiler does **not** have to maintain side-effects that would be mandatory everywhere else under the _as-if_ rule and is allowed to discard them. detecting whether elision is occurring based on the presence or absence of instrumented `cout`s is a tried-and-tested technique. the compiler is not required to keep those `cout`s around. – underscore_d Sep 04 '16 at 19:14
  • Does this answer change with C++17? – Peter - Reinstate Monica Apr 29 '19 at 03:26
  • @PeterA.Schneider In C++17, the copy elision for the first copy becomes mandatory rather than optional. – interjay Apr 29 '19 at 10:26
  • @interjay, do you know the name of the technique for "the copy elision for the first copy becomes mandatory rather than optional."? Thanks – dragonxlwang Jan 05 '21 at 23:02
  • And also, is the first copy elision guaranteed? – dragonxlwang Jan 05 '21 at 23:10
5

It should, but I fail to make it work (live example). The compiler may detect the side-effect of the constructors and decide not to go with copy elision.

#include <iostream>

struct Range{
    Range(double from, double to) : from(from), to(to) { std::cout << "Range(double,double)" << std::endl; }
    Range(const Range& other) : from(other.from), to(other.to) { std::cout << "Range(const Range&)" << std::endl; }
    double from;
    double to;
};

struct Box{
    Box(Range x, Range y) : x(x), y(y) { std::cout << "Box(Range,Range)" << std::endl; }
    Box(const Box& other) : x(other.x), y(other.y) { std::cout << "Box(const Box&)" << std::endl; }
    Range x;
    Range y;
};


int main(int argc, char** argv)
{
    (void) argv;
    const Box box(Range(argc, 1.0), Range(0.0, 2.0));
    std::cout << box.x.from << std::endl;
    return 0;
}

Compile & run:

clang++ -std=c++14 -O3 -Wall -Wextra -pedantic -Werror -pthread main.cpp && ./a.out

Output:

Range(double,double)
Range(double,double)
Range(const Range&)
Range(const Range&)
Box(Range,Range)
1
YSC
  • 38,212
  • 9
  • 96
  • 149
  • 2
    But you are not printing from a copy constructor. – Museful Nov 23 '15 at 13:26
  • 3
    This does elide the two copy constructors going into the by-value constructor, it just doesn't elide the copy from the parameters into the `x` and `y` members. A common pattern is to take by-value then `std::move` into the data members. – TartanLlama Nov 23 '15 at 14:17
  • 2
    @YSC Copy elision doesn't care about side effects. That is the whole point. – juanchopanza Nov 23 '15 at 14:46
  • anyway, if you use `const Range &` you're sure that there's no copy. I wouldn't rely on those optimization unless it's not my code and I can't rework it – Jean-François Fabre Aug 28 '20 at 09:35
5

Yes it can, In particular this kind of copy elision context falls under the copy elision criterion specified in 12.8/p31.3 Copying and moving class objects [class.copy] of the standard:

(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 type (ignoring cv-qualification), the copy/move operation can be omitted by constructing the temporary object directly into the target of the omitted copy/move.

Any descent compiler apply copy elision in this particular context. However, in the OP example two copies taking place.

  1. The temporary objects passed in the constructor (That can be elide per standard as mentioned above).
  2. The copies in the Box constructor's initializer list (That can't be elided).

You can see it in this demo where the copy constructor is evoked only 2 times.

Have also in mind that, because the standard allows in a particular context copy elision optimization, doesn't mean that a compiler vendor is obligated to do it. Copy elision is the only allowed form of optimization that can change the observable side-effects. Consequently, due to the fact that some compilers do not perform copy elision in every situation where it is allowed (e.g., in debug mode), programs that rely on the side-effects of copy/move constructors and destructors are not portable.

101010
  • 41,839
  • 11
  • 94
  • 168
  • 3
    I can't get it to happen. An example would go a long way towards convincing me. – Museful Nov 23 '15 at 14:18
  • 1
    The down-voters would be so kind to explain the reason of their down-vote? If there's really any...? – 101010 Nov 23 '15 at 16:31
  • I didn't downvote, but the downvoters probably came from [this question](http://stackoverflow.com/questions/33873047/will-any-compiler-actually-ever-elide-these-copies) which links to your answer. I suppose they downvoted because your answer was partly incorrect before you edited (the question asks if the copies can be completely elided, to which to answer should be no). – interjay Nov 23 '15 at 18:38
  • @101010, is the "12.8/p31.3" copy elision mandatory? – dragonxlwang Jan 05 '21 at 23:04
  • @dragonxlwang https://en.cppreference.com/w/cpp/language/copy_elision#:~:text=C%2B%2B17)-,Non%2Dmandatory%20elision%20of%20copy%2Fmove%20(since%20C%2B%2B,destructor%20have%20observable%20side%2Deffects.] check mandatory. – 101010 Jan 05 '21 at 23:28
  • just to be clear: is this the part "in the initialization of an object, when the initializer expression is a prvalue of the same class type (ignoring cv-qualification) as the variable type:", so it has become guaranteed since c++17 – dragonxlwang Jan 05 '21 at 23:50
  • @dragonxlwang yeap, for the cases that is mandatory. – 101010 Jan 05 '21 at 23:53
1

The fact that it can, doesn't mean it most certainly will. See it in this Demo, it's obvious you are creating two copies. Hint, the output contains twice :

copy made

copy made

Community
  • 1
  • 1
Lorah Attkins
  • 5,331
  • 3
  • 29
  • 63
  • I don't think that's a fair test. The compiler **must** follow the as-if rule and emit code that calls the `cout`s you put in the code. So you have introduced a condition that prevents the copies from being elided. – Evan Teran Nov 23 '15 at 14:55
  • 3
    @EvanTeran not true. copy elision is the one case where the compiler does **not** have to maintain side-effects that would be mandatory everywhere else under the _as-if_ rule and is allowed to discard them. – underscore_d Sep 04 '16 at 19:11
  • @underscore_d, interesting, thanks for the correction. – Evan Teran Sep 09 '16 at 13:33