Copy elision for pass-by-value arguments
Asked Answered
F

4

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?)

Fostoria answered 23/11, 2015 at 13:15 Comment(0)
E
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;
Escape answered 23/11, 2015 at 14:23 Comment(11)
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.Snaky
@MatthieuM. I've edited to hopefully make the distinction clearer.Escape
"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.Daile
@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, 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.Escape
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?Daile
@JordanMelo Correct. The constructor parameters from and to are named variables and therefore not temporaries.Escape
@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 couts is a tried-and-tested technique. the compiler is not required to keep those couts around.Traduce
Does this answer change with C++17?Davidadavidde
@PeterA.Schneider In C++17, the copy elision for the first copy becomes mandatory rather than optional.Escape
@interjay, do you know the name of the technique for "the copy elision for the first copy becomes mandatory rather than optional."? ThanksTheaterintheround
And also, is the first copy elision guaranteed?Theaterintheround
A
6

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.

Apocrypha answered 23/11, 2015 at 13:21 Comment(7)
I can't get it to happen. An example would go a long way towards convincing me.Fostoria
The down-voters would be so kind to explain the reason of their down-vote? If there's really any...?Apocrypha
I didn't downvote, but the downvoters probably came from this question 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).Escape
@101010, is the "12.8/p31.3" copy elision mandatory?Theaterintheround
@Theaterintheround en.cppreference.com/w/cpp/language/… check mandatory.Apocrypha
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++17Theaterintheround
@Theaterintheround yeap, for the cases that is mandatory.Apocrypha
L
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
Longtin answered 23/11, 2015 at 13:19 Comment(4)
But you are not printing from a copy constructor.Fostoria
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.Grieve
@Longtin Copy elision doesn't care about side effects. That is the whole point.Seamark
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 itDichroic
A
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

Avid answered 23/11, 2015 at 13:30 Comment(3)
I don't think that's a fair test. The compiler must follow the as-if rule and emit code that calls the couts you put in the code. So you have introduced a condition that prevents the copies from being elided.Foretopsail
@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.Traduce
@underscore_d, interesting, thanks for the correction.Foretopsail

© 2022 - 2024 — McMap. All rights reserved.