C++ Copy Elision Optimization

Introduction

Copy elision is a common compile-time optimization in C++. In this blog post, I would like to show some examples of copy elision, discuss its mechanism at the compiler level and some of its caveats.

Copy Elision

Copy elision omits copy and move constructors, resulting in zero-copy pass-by-value semantics.

Examples

copy_elision.cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
#include <iostream>

class Dummy
{
public:
Dummy(const std::string& name = "") : m_name{name}
{
std::cout << "Constructor being called." << std::endl;
};
Dummy(const Dummy& rhs) : m_name{rhs.m_name}, m_value{rhs.m_value}
{
std::cout << "Copy constructor being called." << std::endl;
};
Dummy& operator=(const Dummy& rhs)
{
std::cout << "Copy assignment being called." << std::endl;
m_name = rhs.m_name;
m_value = rhs.m_value;
return *this;
};
Dummy(Dummy&& rhs) noexcept
{
std::cout << "Move constructor being called." << std::endl;
m_name = std::move(rhs.m_name);
m_value = rhs.m_value;
rhs.m_value = 0;
};
Dummy& operator=(Dummy&& rhs) noexcept
{
std::cout << "Move assignment being called." << std::endl;
m_name = std::move(rhs.m_name);
m_value = rhs.m_value;
rhs.m_value = 0;
return *this;
};

private:
std::string m_name;
int m_value;
};

Dummy f()
{
Dummy a{"A"};
std::cout << "Memory address of `a`: " << &a << std::endl;
return a;
}

void g()
{
Dummy b{f()};
std::cout << "Memory address of `b`: " << &b << std::endl;
}

void h(Dummy c) { std::cout << "Memory address of `c`: " << &c << std::endl; }

int main()
{
std::cout << "=====================================" << std::endl;
g();
std::cout << "-------------------------------------" << std::endl;
h(f());
std::cout << "=====================================" << std::endl;
}

Usually, compilers, such as GCC, will enable copy elision optimization automatically. We could see from the following printouts that copy elision saves us a few move (or copy) operations from return value or pass-by-value.

1
2
3
4
5
6
7
8
9
10
11
$ g++ copy_elision.cpp -o copy_elision -std=c++14
$ ./copy_elision
=====================================
Constructor being called.
Memory address of `a`: 0x7ffff88f8bc0
Memory address of `b`: 0x7ffff88f8bc0
-------------------------------------
Constructor being called.
Memory address of `a`: 0x7ffff88f8c10
Memory address of `c`: 0x7ffff88f8c10
=====================================

The GCC option -fno-elide-constructors allows us to disable copy elision so that we could see what’s happening without copy elision.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
$ g++ copy_elision.cpp -o copy_elision -std=c++14 -fno-elide-constructors
$ ./copy_elision
=====================================
Constructor being called.
Memory address of `a`: 0x7ffee0d019f0
Move constructor being called.
Move constructor being called.
Memory address of `b`: 0x7ffee0d01a40
-------------------------------------
Constructor being called.
Memory address of `a`: 0x7ffee0d01a70
Move constructor being called.
Move constructor being called.
Memory address of `c`: 0x7ffee0d01af0
=====================================

Sometimes, the copy elision optimization could be disabled or compromised by some erroneous implementations without being aware by the user. In the following example ineffective_copy_elision.cpp, the only difference to copy_elision.cpp is that we return a rvalue reference std::move(a) instead of a value a in f() to enable a move instead of copy to the return value.

ineffective_copy_elision.cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
#include <iostream>

class Dummy
{
public:
Dummy(const std::string& name = "") : m_name{name}
{
std::cout << "Constructor being called." << std::endl;
};
Dummy(const Dummy& rhs) : m_name{rhs.m_name}, m_value{rhs.m_value}
{
std::cout << "Copy constructor being called." << std::endl;
};
Dummy& operator=(const Dummy& rhs)
{
std::cout << "Copy assignment being called." << std::endl;
m_name = rhs.m_name;
m_value = rhs.m_value;
return *this;
};
Dummy(Dummy&& rhs) noexcept
{
std::cout << "Move constructor being called." << std::endl;
m_name = std::move(rhs.m_name);
m_value = rhs.m_value;
rhs.m_value = 0;
};
Dummy& operator=(Dummy&& rhs) noexcept
{
std::cout << "Move assignment being called." << std::endl;
m_name = std::move(rhs.m_name);
m_value = rhs.m_value;
rhs.m_value = 0;
return *this;
};

private:
std::string m_name;
int m_value;
};

Dummy f()
{
Dummy a{"A"};
std::cout << "Memory address of `a`: " << &a << std::endl;
return std::move(a);
}

void g()
{
Dummy b{f()};
std::cout << "Memory address of `b`: " << &b << std::endl;
}

void h(Dummy c) { std::cout << "Memory address of `c`: " << &c << std::endl; }

int main()
{
std::cout << "=====================================" << std::endl;
g();
std::cout << "-------------------------------------" << std::endl;
h(f());
std::cout << "=====================================" << std::endl;
}

However, this explicit operation disabled copy elision optimization.

1
2
3
4
5
6
7
8
9
10
11
12
13
$ g++ ineffective_copy_elision.cpp -o ineffective_copy_elision -std=c++14
$ ./ineffective_copy_elision
=====================================
Constructor being called.
Memory address of `a`: 0x7ffe439d6a60
Move constructor being called.
Memory address of `b`: 0x7ffe439d6ab0
-------------------------------------
Constructor being called.
Memory address of `a`: 0x7ffe439d6ab0
Move constructor being called.
Memory address of `c`: 0x7ffe439d6b00
=====================================

Copy Elision Optimization from Compiler

Jon Kalb has created a great presentation on copy elision to facilitate its understanding at the compiler level.

Return Value Pass-By-Value Optimization

Return Value Pass-By-Value Before Optimization
Return Value Pass-By-Value After Optimization

Argument Passing Pass-By-Value Optimization

Argument Passing  Pass-By-Value Before Optimization
Argument Passing Pass-By-Value After Optimization

Caveats

Interestingly, if the copy constructor of a class does partial copy or the move constructor of a class does partial move, the behavior of the application might be different between copy elision enabled and disabled.

In addition, functions in which the variable being returned is not determined at compile time cannot benefit from copy elision. For example,

1
2
3
4
5
6
7
8
9
10
11
12
13
Dummy f(bool v)
{
Dummy a{"A"};
Dummy b{"B"};
if (v)
{
return a;
}
else
{
return b;
}
}

Conclusions

Just write pass-by-value in C++ programs naturally and the compiler will do the optimization for you.

References

Author

Lei Mao

Posted on

09-05-2022

Updated on

09-05-2022

Licensed under


Comments