WEBVTT

00:00.120 --> 00:03.330
Hello again! In this video, we are going to look at copy elision.

00:03.720 --> 00:09.180
This is a form of optimization in which the compiler misses out calls to the copy constructor.

00:11.470 --> 00:17.950
As a very simple example, imagine we have an int x with value 7 and then we use this x to initialize another

00:18.010 --> 00:18.700
variable, y.

00:19.630 --> 00:23.290
And then we never actually use x again until it goes out of scope.

00:26.040 --> 00:33.390
It is very easy for compilers to see that this variable X is not actually needed, and instead of initializing

00:33.390 --> 00:38.970
this variable x and then copying it into y, they can directly create the variable y with the initial

00:38.970 --> 00:39.330
value,

00:39.360 --> 00:39.810
seven.

00:42.540 --> 00:49.860
And this also applies to objects. If we have, for example, a string which is used to initialize another

00:49.860 --> 00:57.330
string, and nothing else, then the compiler can skip over this copy and it will initialize the permanent

00:57.330 --> 00:58.530
variable directly.

00:59.310 --> 01:05.430
So this will create a single object which has the initial value, as opposed to creating two objects,

01:05.790 --> 01:07.140
one of which is a copy of the other.

01:10.220 --> 01:16.640
So the first one, that means it has to call the constructor for "temp", then the copy constructor for "perm".

01:16.640 --> 01:23.780
And then when "temp" goes out of scope, it will need  to call the destructor for that. With the

01:23.780 --> 01:24.560
optimized form,

01:24.560 --> 01:29.630
It just needs to call the constructor for "perm", and then the destructor as well.

01:29.630 --> 01:31.640
But it was going to call the destructor in that case as well.

01:31.940 --> 01:37.940
So we have actually saved a call to the copy constructor and a call to the destructor. And as these involve

01:37.940 --> 01:41.000
memory operations, they take a lot of processor cycles.

01:41.600 --> 01:43.430
So that is quite a useful optimization.

01:45.350 --> 01:50.930
So this is copy elision: the compiler is allowed to skip over the call to the copy constructor and it

01:50.930 --> 01:53.390
can initialize the target object directly.

01:54.140 --> 01:59.750
The usual case where this comes up is with temporary variables and function calls; either copying temporary

01:59.750 --> 02:05.270
variables into function arguments, or copying temporary variables into the function's return value.

02:07.320 --> 02:12.870
Normally in C++, the compiler cannot perform optimizations which change the behaviour of the program.

02:14.040 --> 02:15.690
That is called the "as-if" rule.

02:16.050 --> 02:19.860
So the program must behave "as if" the optimization had not happened.

02:20.220 --> 02:23.760
So the only thing the optimization can do is to make the program run faster.

02:24.570 --> 02:30.180
However, with copy elision, the compiler is allowed to omit the call, even if the copy

02:30.180 --> 02:31.650
constructor has side effects.

02:32.010 --> 02:34.530
So in that case, it will change the behaviour of the program.

02:36.240 --> 02:40.920
Modern compilers will always perform copy elision if they can. So if you do not want it, you have to

02:40.920 --> 02:41.550
turn it off.

02:42.390 --> 02:49.780
For most compilers, there -s an option, "minus f-no-elide-constructors". For visual C++,

02:49.800 --> 02:58.260
the documentation says you have to disable all optimizations, "/Od", but that does not

02:58.260 --> 02:58.830
work for me.

03:03.500 --> 03:06.410
So here is an example of this. We have a class, Test.

03:07.160 --> 03:08.630
It has a copy constructor.

03:10.380 --> 03:13.800
This prints out a message, so this has a side effect.

03:14.130 --> 03:17.760
It changes the state of the standard output stream object.

03:18.960 --> 03:20.880
We define a default construction.

03:21.210 --> 03:22.890
We actually have to define this.

03:23.040 --> 03:25.050
And you might like to think about why that is.

03:27.240 --> 03:33.990
The reason is that we define a copy constructor. And if we define any constructor, including a copy

03:33.990 --> 03:41.040
constructor, the compiler will not synthesize a default constructor. And we need a constructor, so

03:41.040 --> 03:42.150
we have to implement one.

03:43.110 --> 03:47.610
And for good measure, we  are going to print out a message as well, so we can see what is going on.

03:49.520 --> 03:55.370
We have a non-member function, which returns a temporary object of this class. So it calls the constructor

03:55.370 --> 03:56.990
and then passes that to return.

03:57.620 --> 04:03.110
So this will create a temporary object of class Test. In the normal course of events,

04:03.140 --> 04:05.480
this would be copied into the function's return space.

04:09.110 --> 04:16.940
In the main function, we use this return value to initialize a variable of this class, so that is a

04:17.720 --> 04:18.890
copy initialization.

04:19.190 --> 04:25.010
So that will call the copy constructor, again with the temporary object in the return space as the argument.

04:25.370 --> 04:27.470
So that is 2 calls to the copy constructor.

04:27.770 --> 04:30.110
So we expect to see "Copying" appear twice.

04:30.830 --> 04:33.380
Or do we? What do you think will happen?

04:38.480 --> 04:44.090
So we get no calls at all. So let's try this again with GCC.

04:44.420 --> 04:46.430
I have the same code here.

04:47.720 --> 04:51.770
If we compile this with this option, -f-no-elide-constructors,

04:54.160 --> 04:55.020
and then we run it.

04:55.570 --> 05:01.450
So we do get the two calls to the copy constructor. So we have the default constructor call in here

05:02.050 --> 05:07.510
and then that will copy the temporary object into the return space, and then it will copy it again

05:07.510 --> 05:17.380
into the Test object in main(). If I compile it without that option, then we get the same result

05:17.380 --> 05:19.390
as we did with Visual C++.

05:23.540 --> 05:29.350
So the copy elision has taken place twice. The first time when the temporary object is copied into the function's

05:29.400 --> 05:35.260
return space. That has been elided. And that is known as a return value optimization. And also the copy

05:35.270 --> 05:37.760
initialization of the variable in main() was avoided.

05:42.230 --> 05:47.660
And then the return value optimization, that is when the.. I think I am rather repeating myself here!

05:48.530 --> 05:53.140
But the advantage of this is we have avoided one call to the copy constructor and one call to the

05:53.140 --> 05:53.720
destructor.

05:55.150 --> 06:01.120
And then, similarly, for the value in main(), the copy elision again.

06:01.180 --> 06:04.750
That has avoided another call to the copy constructor and destructor.

06:05.380 --> 06:11.740
So that is 4 special member calls which have been avoided. The disadvantages is that the side effect

06:11.740 --> 06:15.340
of the copy constructor, the one where it prints out the message, has not occurred.

06:17.500 --> 06:23.860
There is also a similar process if you have a local variable with a name and it is returned by value, and

06:23.860 --> 06:26.470
that is known as "Named Return Value Optimization".

06:27.220 --> 06:28.990
This is a bit harder for compilers to do.

06:29.360 --> 06:32.830
Compilers will always do return value optimization for a temporary object.

06:33.160 --> 06:34.750
But when you have a variable with a name,

06:34.750 --> 06:39.760
it can be more difficult for compilers to work out what is happening, especially if there are branches or exceptions

06:39.760 --> 06:41.800
or things like that to confuse the issue.

06:47.050 --> 06:48.310
So here is an example again.

06:48.580 --> 06:55.810
The code is exactly the same, except we are now returning a local variable. So we have this local variable,

06:55.840 --> 06:58.540
which is an object of the class, and we return that by value.

06:59.530 --> 07:01.720
And this time we have a different result.

07:01.720 --> 07:05.590
We see copying once, instead of no times at all.

07:06.100 --> 07:07.900
So there has been a copy constructor call somewhere.

07:08.530 --> 07:15.910
And what has happened is that the compiler has not been able to optimize away the copy of the local

07:15.910 --> 07:18.400
variable in the function, into the function's return space.

07:18.880 --> 07:25.150
So it has actually had to call the copy constructor there. But in the actual initialization of the variable

07:25.150 --> 07:27.700
in main(), it has been able to optimize away that copy.

07:28.030 --> 07:29.080
So we see it once.

07:31.540 --> 07:36.130
And finally, the copy elision can also occur when a temporary object is passed by value.

07:38.290 --> 07:40.270
So here we have the same class again.

07:43.140 --> 07:47.880
We have a function which takes an object of this class by value, so it is going to be copied into the

07:47.880 --> 07:48.900
function argument.

07:49.920 --> 07:57.030
We have a main() function, which calls this function, and it passes a temporary object as the argument.

07:58.110 --> 08:04.710
So normally this temporary object would be copied into the function argument, so we would expect to see one

08:05.130 --> 08:07.110
occurrence of "Copying" in the output.

08:07.950 --> 08:10.170
So what do you think will happen this time?

08:11.640 --> 08:14.490
I think you might have guessed that the answer is zero, again.

08:16.260 --> 08:21.150
If I do this in g++, again with the -f-no-elide-constructors option.

08:23.990 --> 08:25.910
Then we get to one call to "Copying".

08:26.360 --> 08:29.390
So that's one call to the copy constructor that has been elided.

08:31.130 --> 08:32.820
Okay, so that is it for this feature.

08:33.620 --> 08:36.410
I will see you next time, but until then, keep coding!
