Copy elision
Copy elision is a compiler optimization technique in the C++ programming language that allows the omission of copy and move constructions for objects of class types under specific conditions, treating the source and target objects as identical even if the constructors or destructors exhibit observable side effects.[1] This optimization is permitted in contexts such as return statements, throw expressions, coroutine parameters, and exception handlers, where one object is designated for destruction instead of two, ensuring that the program's observable behavior remains unchanged except for the number of constructions and destructions.[1]
The origins of copy elision trace back to a 1995 proposal by Andrew Koenig, which introduced "copy optimization" to eliminate unnecessary object copies in function returns and initializations as part of the evolving C++ standard.[2] Incorporated into the first C++ standard (ISO/IEC 14882:1998), it initially focused on optional forms like return value optimization (RVO)—eliding copies of unnamed temporaries in return statements—and named return value optimization (NRVO)—extending this to named local variables—though implementation was not mandatory and depended on compiler heuristics.[1] Prior to C++11, copy elision was the sole permitted optimization altering program semantics in this manner; subsequent standards added allowances for move elision and allocation elision.
Significant advancements occurred in C++17 with the adoption of guaranteed copy elision, which redefined prvalue materialization to mandate elision in cases like direct initialization from temporaries or prvalues, eliminating the need for copy or move constructors in those scenarios and enabling non-movable types to be returned efficiently.[3] This change, driven by WG21 paper P0135R0, simplified the language model by deferring temporary object creation until necessary, reducing overhead and improving code portability across compilers.[3] However, elision remains prohibited in constant expressions and certain initialization contexts to preserve strict semantics.[1]
Fundamentals
Definition and Mechanism
Copy elision is a compiler optimization in C++ that allows the omission of copy or move constructors and destructors for class objects under specific circumstances, even if those operations have observable side effects.[4][1] In this process, the compiler treats the source object and the target object as identical, constructing the object directly in its final storage location and adjusting the lifetime accordingly, thereby avoiding the creation of temporary objects.[4][1]
The mechanism of copy elision involves eliding the invocation of the copy or move constructor when a new object would otherwise be created from an existing one of the same type, provided the elision does not alter the observable behavior of the program.[5] For instance, if the first parameter of the selected constructor is an rvalue reference to the object's type, the destruction of that object occurs when the target would have been destroyed; otherwise, the destruction occurs at the later of the times when the two objects would have been destroyed without the optimization.[4] This optimization is permitted only in non-constant expressions and is restricted in constant initialization contexts to preserve program semantics.[6]
Elidable copy contexts, as defined in the C++ standard, include several scenarios where the compiler may or must omit such operations. These encompass the initialization of a variable as the result of a return statement from a function returning that variable's type, where a non-volatile automatic object is directly constructed into the caller's return object.[4][7] Another context is a throw-expression initializing an exception object from a non-volatile automatic object outside a try-block's scope.[4][7] Additionally, in exception-declarations, the declared object may alias the exception object without copying, and since C++20, coroutine parameters can elide copies if program meaning remains unchanged.[4][7] Prior to C++17, temporary materialization in copy-initialization could also be elided optionally.[4]
Copy elision has been part of the C++ standard since C++98, where it was permitted but not required in cases like named return value optimization (NRVO).[4] The C++11 standard expanded it to include throw-expressions and exception handlers, refining lifetime rules.[4] In C++17, certain forms became mandatory through changes to prvalue semantics, guaranteeing elision for unnamed return value optimization (URVO) and temporary materializations without requiring copyable types, even if constructors are deleted.[4][1] This shift ensures that copies or moves are elided when a temporary object is copied or moved to another object, provided it meets the elision criteria.[4]
Copy elision offers substantial performance advantages by eliminating invocations of copy or move constructors, which are particularly costly for complex objects like strings or vectors that involve deep copying of internal data structures.[4] This avoidance enables zero-cost abstractions, where the runtime overhead of returning or initializing objects by value matches that of direct construction, without the need for programmers to resort to less expressive reference or pointer semantics.[8]
Without copy elision, repeated object copies in scenarios such as loops or recursive functions can accumulate significant overhead, whereas elision constructs objects directly in their target locations, substantially reducing execution time. Additionally, it minimizes resource consumption by preventing the allocation of temporary objects and the corresponding calls to destructors, thereby lowering both memory footprint and CPU cycles dedicated to cleanup.[4]
Prior to C++11, copy elision facilitated zero-copy returns through techniques like return value optimization, rendering value-based returns as efficient as returning references in terms of performance.[8] This optimization ensured that even non-trivial objects could be returned without the expense of copying, promoting efficient value semantics across a wide range of applications.
Elision Contexts
Return Statements
In C++, copy elision applies to return statements when a function returns an object by value, allowing the compiler to construct the object directly within the caller's designated return storage rather than creating a separate temporary and copying or moving it. This optimization is possible if the return expression names a local automatic object with automatic storage duration or a temporary object, thereby eliminating unnecessary constructor and destructor invocations.[9]
For elision to occur, the type of the return expression must match the function's return type exactly, disregarding only cv-qualifications such as const or volatile. Elision fails if the types differ or if the return involves user-defined conversions, including those via conversion constructors or operators, in which case a copy or move constructor must be invoked instead.[10]
This form of elision constitutes the core of return value optimization, a feature permitted by the C++ standard since its 1998 edition.[9]
In cases of unnamed return values, such as return make_vector(); where the expression yields a prvalue, the compiler may elide the copy prior to C++17, but this is not required and depends on the implementation. Starting with C++17, such elision is guaranteed, as prvalues no longer materialize into temporaries but directly initialize the caller's return object.[9]
Exception Handling
In the context of exception handling in C++, copy elision optimizes the propagation of exceptions by allowing the exception object to be constructed directly from the thrown expression, omitting any copy from a temporary, and by treating the catch handler's parameter as an alias for the exception object, thereby avoiding a copy during the catch operation. This mechanism applies when the throw expression names a non-volatile automatic object with automatic storage duration (other than a function parameter or catch-clause parameter) of the same cv-unqualified type as the exception type and outside the scope of the innermost try-block, where the automatic object is constructed directly into the exception object. For prvalues in throw expressions, the exception object is directly initialized (since C++17, without materializing a temporary), and no copy elision from a temporary is needed in the same manner.[1]
The conditions for elision in the catch handler require that the copy initialization of the handler parameter from the exception object would not change the program's meaning beyond omitting constructors and destructors, typically when they share the same cv-unqualified type to avoid slicing or loss of information. For named automatic objects in throw expressions—non-volatile and outside the innermost try-block—the object can be constructed in situ into the exception storage, provided it is not a function parameter or handler variable. This elision has been permitted since the C++98 standard, targeting the overhead in exception paths that, while infrequent, incur significant costs due to stack unwinding and object management.[1][11]
During propagation, particularly with rethrow statements (throw;), no additional copy occurs if the caught exception object was elided into the handler parameter, as the parameter acts as an alias for the original exception object, preserving its lifetime and state across handler boundaries. This aliasing ensures that modifications in the handler reflect back to the propagating exception, maintaining program semantics without invoking copy or move operations. Such optimizations reduce the runtime expense of exception handling, especially for complex objects, by minimizing constructor and destructor invocations along the error path.[1][12]
Initializations
Copy elision in object initializations occurs when a compiler omits the copy or move construction of a temporary object that would otherwise initialize a target object of the same class type, constructing the temporary directly into the target's storage instead. This optimization is permitted under the C++ standard when a temporary class object, not bound to a reference, is copied or moved to a class object with the same cv-unqualified type as the temporary.[1] By treating the source and target as a single object, the compiler avoids unnecessary constructions while preserving the observable behavior, including side effects from constructors or destructors that are elided.[1]
A common case arises in direct initialization where a prvalue expression produces a temporary that initializes an object of the same type. For instance, in the declaration Foo f(g());—assuming g() returns a prvalue of type Foo—the temporary resulting from g() can be elided, allowing direct construction into f without invoking a copy or move constructor.[1] This applies similarly to copy initialization forms like Foo f = g();, where the temporary from the prvalue is constructed straight into the target object, reducing overhead in scenarios involving function calls or complex expressions that yield temporaries.[1] Such elision is particularly beneficial for expensive-to-construct types, as it eliminates the double construction that would otherwise occur.
Elision also extends to initialization lists, including member and base class initializers in constructors, where temporaries passed as arguments can be bound directly to the target subobjects if they share the same type. In a constructor's initializer list, if a temporary expression of type T initializes a member or base of type T, the compiler may construct the temporary directly into that subobject, omitting the copy or move.[1] This rule ensures that temporaries in constructor arguments are not redundantly copied when they align with the target's lifetime and type requirements.
The introduction of uniform initialization in C++11 further clarified and enhanced copy elision opportunities for braced-init-lists, enabling direct construction in list-initialization contexts that would previously involve additional copies. Under C++11's list-initialization rules, when a braced-init-list selects a copy or move constructor for the target type, elision can occur by constructing the elements directly into the object, thus reducing copies in aggregate and class initializations. This clarification promotes more efficient uniform syntax, such as Foo f{args};, where temporaries from the initializer list integrate seamlessly without intermediate objects. Overall, these mechanisms ensure that initializations leveraging temporaries remain performant by prioritizing direct binding over explicit copies or moves.
Optimizations and Variants
Return Value Optimization
Return Value Optimization (RVO) refers to a specific instance of copy elision applied when a function returns an unnamed temporary object or prvalue by value, allowing the compiler to construct the object directly in the caller's designated return storage location rather than creating and copying a temporary. This eliminates the invocation of the copy or move constructor that would typically occur, thereby avoiding unnecessary overhead in object construction and destruction.[13][4]
The mechanism behind RVO often involves the compiler passing a hidden pointer or reference to the return slot—allocated by the caller—as an implicit argument to the function, enabling direct construction into that storage without intermediate temporaries. This approach may require whole-program analysis to identify eligible return expressions or reliance on standardized calling conventions that support return-by-invisible-reference semantics for non-trivial types. By sharing storage between the callee and caller, RVO ensures the returned object's lifetime aligns with the caller's expectations, mimicking the efficiency of returning a reference while preserving value semantics.[4][13]
Unlike broader forms of copy elision, RVO is narrowly focused on return-by-value scenarios to achieve performance comparable to pass-by-reference, without altering the function's interface or introducing dangling reference risks. It has been a permitted optimization in the C++ standard since the 1998 specification, with widespread implementation in major compilers by the late 1990s, though its application could vary based on factors like function inlining or optimization levels. Prior to C++17, RVO for unnamed returns was optional; since then, it has become mandatory for prvalues, ensuring consistent elision across compliant compilers.[4][13]
Named Return Value Optimization
Named Return Value Optimization (NRVO) is a form of copy elision that allows compilers to construct a named automatic local object directly into the caller's return value slot when it is returned from a function, thereby omitting the subsequent copy or move operation.[1] This optimization applies specifically to functions returning class types by value, where the return statement's expression is the name of a non-volatile object with automatic storage duration that is neither a function parameter nor an exception-declaration variable. By treating the local object and the function's result object as identical, NRVO avoids the invocation of copy or move constructors and destructors that would otherwise occur.[14]
For NRVO to occur, the returned named object must typically be the same across all execution paths in the function, as compilers rely on control flow analysis to ensure the optimization preserves program semantics. Elision is permitted by the standard whenever the return expression meets the basic criteria, but practical implementation often fails in cases involving conditional returns of different objects, loops that alias the object, or exception handling that complicates lifetime tracking.[14] Additionally, the object should not be modified in ways that introduce observable side effects incompatible with direct construction into the return slot, such as taking its address for external use before return.
Compared to Return Value Optimization for unnamed temporaries, NRVO is more conservative because it involves a named entity that may have aliases or conditional usage, necessitating deeper flow analysis by the compiler to verify elision safety.[15] Despite these constraints, NRVO extends elision opportunities to scenarios with complex local computations where unnamed returns are impractical. In contemporary compilers like GCC, Clang, and MSVC, NRVO is routinely combined with other elision techniques to maximize performance in return-heavy code, though it remains optional and may be disabled in debug builds or via flags like /Zc:nrvo-.[16][14]
Standards Evolution
Pre-C++11 Specifications
Copy elision was introduced in the C++98 standard (ISO/IEC 14882:1998) as an optional optimization technique specified in section 12.8, paragraph 15, allowing compilers to omit certain copy constructions of class objects even when the copy constructor or destructor exhibited side effects.[4] In such cases, the implementation could treat the source and target of the elided copy as aliases for the same object, with destruction occurring as if the object were constructed only once.[4] This provision enabled efficient implementation of value semantics, permitting objects to be passed and returned by value without invariably incurring the overhead of copying, while leaving the decision to elide to the compiler's discretion.[4]
The standard explicitly permitted elision in three primary contexts, which could be combined to eliminate multiple copies: first, in a return statement of a function returning a class type, when the returned expression named a non-volatile automatic object (other than a function or catch-clause parameter) with the same cv-unqualified type as the return type, allowing direct construction into the return value; second, when a temporary class object not bound to a reference would be copied to another class object of the same cv-unqualified type, permitting direct construction of the temporary into the target; and third, in a throw-expression, when the thrown operand named a non-volatile automatic object (other than a function or catch-clause parameter) with the same cv-unqualified type as the exception object, allowing direct construction into the exception.[4] These rules applied only to non-volatile objects with automatic storage duration and did not extend to constant expressions or constant initializations.[4]
Key limitations in the C++98 and subsequent C++03 standards restricted elision's applicability, particularly prohibiting it for most named objects outside the specified return and throw contexts, thereby relying on optional techniques such as Return Value Optimization (RVO) for unnamed temporaries and Named Return Value Optimization (NRVO) for named locals.[4] Elision was further barred for volatile objects, function parameters, and catch-clause parameters, ensuring that observable behavior remained consistent unless explicitly elided within the permitted cases.[4] This permissive framework balanced optimization opportunities with predictable program semantics, though it meant that programmers could not rely on elision for performance guarantees.[4]
C++11 and Later Changes
In C++11, the language introduced rvalue references and move semantics, which provided a mechanism to efficiently transfer resources from temporary objects without deep copying when copy elision could not be applied. This interacted with copy elision such that elided copies or moves were skipped entirely, even if std::move was explicitly applied to force a move; the optimization precedence ensured no unnecessary constructor calls occurred.[4] C++11 also permitted copy elision in exception handlers, allowing the omission of copy-initialization when the handler's parameter is initialized from a matching non-volatile automatic object of the same type.[4] As a result, developers could rely on elision to avoid overhead in return statements and initializations, with moves serving as a fallback only when elision was unavailable.
C++14 built on these foundations by permitting allocation elision alongside traditional copy elision as an allowed optimization, reducing overhead in object construction and destruction.[4] Refinements to uniform initialization, originally introduced in C++11, further enabled elision of additional temporaries in list-initialization contexts, such as when initializing aggregates or using braced-init-lists, by treating them more uniformly without implicit conversions that might trigger copies. If elision failed in these scenarios, automatic move semantics ensured that rvalue-like behavior was invoked preferentially over copying, minimizing runtime costs for movable types.
The most significant evolution occurred in C++17, which mandated guaranteed copy elision for prvalues in specific contexts, such as return statements and variable initializations from prvalue expressions.[4] Under these rules, prvalues are no longer materialized as temporary objects but are constructed directly into the storage of their target, eliminating any potential copy or move operations.[3] This guarantee applies particularly to returning prvalues from functions, where the returned object is built in the caller's return slot without intermediate temporaries. Elision's precedence over move semantics means that even non-movable or non-copyable types can participate without requiring move constructors, as no such operations are semantically required, thereby extending the optimization's applicability to a broader range of user-defined types.[4][17]
C++20 extended copy elision to coroutine contexts, permitting the omission of copies for coroutine parameters that are non-volatile automatic objects with the same cv-unqualified type as the parameter.[4]
Compiler Implementations
Major Compilers
GCC has supported full return value optimization (RVO) and named return value optimization (NRVO) since version 4.1,[18] with these optimizations enabled by default and controllable via the -fno-elide-constructors flag.[19] As of GCC 14 (2023), further NRVO enhancements improve elision in more complex cases.[20] In C++17 mode, GCC provides guaranteed copy elision as required by the standard starting from version 7, indicated by the predefined macro __cpp_guaranteed_copy_elision with value 201606L.[21]
Clang implements aggressive copy elision, including NRVO in most cases, since version 3.0, aligning closely with the C++ standard's permissions for optional optimizations.[22] For C++17's guaranteed copy elision (P0135R1), support was added in Clang 4.0, ensuring prvalue expressions are constructed directly in their target storage without temporary materialization.[22]
Microsoft Visual C++ (MSVC) has included RVO at least since Visual Studio 2005,[15] with improvements to NRVO implementation in Visual Studio 2015, allowing more consistent elision for named local variables returned from functions.[23] Full support for C++17 guaranteed copy elision arrived in Visual Studio 2017 (version 15.6), where mandatory elision occurs regardless of optimization settings, while optional NRVO can be explicitly controlled via the /Zc:nrvo flag introduced in Visual Studio 2022 version 17.4.[17][24]
All major C++ compilers—GCC, Clang, and MSVC—enable copy elision by default without requiring special flags, though higher optimization levels such as -O2 or /O2 typically enhance the scope and aggressiveness of these optimizations by enabling additional passes that identify elision opportunities.[19][24][25]
Verification Methods
To verify whether copy elision is occurring in a C++ program, developers can use compiler flags to disable the optimization and compare the resulting behavior against the default enabled case. In GCC and Clang, the -fno-elide-constructors flag disables copy elision, forcing the compiler to invoke copy or move constructors even in scenarios where elision would otherwise apply, such as return value optimization.[19][26] By compiling the same code with and without this flag—typically under optimization levels like -O2—and observing differences in execution (e.g., via output or runtime metrics), one can confirm if elision was active in the optimized build.
Debugging techniques provide direct observation of constructor invocations to confirm elision. A common method involves instrumenting the copy and move constructors of the class in question with logging statements, counters, or side-effect producing code (such as incrementing a global variable or printing to stdout) to track how many times they are called during program execution.[19] If elision occurs, these instrumented functions will not be invoked for the elided copies, allowing verification through runtime inspection; this approach is particularly useful when combined with the -fno-elide-constructors flag to establish a baseline where calls are expected.
Static analysis tools and compiler warnings offer compile-time detection of potential elision issues or missed opportunities. Clang emits warnings such as "moving a local object in a return statement prevents copy elision" when code patterns (e.g., using std::move on a return operand) inhibit elision, helping identify cases where optimization might not apply.[27] Similarly, GCC's -Wpessimizing-move flag warns about std::move usage that blocks elision in return contexts.[19] Tools like Clang-Tidy can further assist through checks such as performance-unnecessary-copy-initialization, which flags redundant copies that elision could avoid, though it focuses more on initialization patterns than elision directly.[28]
In C++17 and later, guaranteed copy elision for prvalues makes the behavior observable without relying on optional optimizations, as prvalues initialize objects directly without materializing temporaries, eliding any copy or move even if constructors have side effects.[9] This can be verified using side-effect tests, such as placing observable actions (e.g., logging or counter increments) in the copy/move constructors and confirming they are not executed when returning a prvalue, distinguishing it from pre-C++17 optional elision where side effects might inconsistently appear.[9]
Practical Examples
Simple Return Case
In the simple return case, a function returns a prvalue, such as a temporary object created by an expression like a constructor call or operation result, without involving named local variables. Copy elision permits the compiler to omit the copy (or move) of this temporary by constructing it directly into the caller's designated storage, as specified in the C++ standard's copy elision rules. This optimization, often realized through return value optimization (RVO), reduces overhead by eliminating unnecessary object creations and destructions.
A representative example involves a function returning the result of a temporary string concatenation, demonstrated using a custom class Str that mimics basic string behavior and tracks constructions and copies via static counters:
cpp
#include <iostream>
#include <cstring>
struct Str {
static int construct_count;
static int copy_count;
char* data;
Str(const char* s) : data(new char[strlen(s) + 1]) {
strcpy(data, s);
++construct_count;
std::cout << "Construction\n";
}
Str(const Str& other) : data(new char[strlen(other.data) + 1]) {
strcpy(data, other.data);
++copy_count;
std::cout << "Copy construction\n";
}
Str operator+(const Str& other) const {
size_t len = strlen(data) + strlen(other.data) + 1;
char* new_data = new char[len];
strcpy(new_data, data);
strcat(new_data, other.data);
Str result(new_data);
delete[] new_data; // Temporary allocation for + result
return result;
}
~Str() {
delete[] data;
std::cout << "Destruction\n";
}
};
int Str::construct_count = 0;
int Str::copy_count = 0;
Str make_concat() {
Str temp1("hello");
Str temp2(" world");
return temp1 + temp2; // Returns temporary from operator+
}
int main() {
Str result = make_concat();
std::cout << "Final counts - Constructs: " << Str::construct_count
<< ", Copies: " << Str::copy_count << "\n";
return 0;
}
#include <iostream>
#include <cstring>
struct Str {
static int construct_count;
static int copy_count;
char* data;
Str(const char* s) : data(new char[strlen(s) + 1]) {
strcpy(data, s);
++construct_count;
std::cout << "Construction\n";
}
Str(const Str& other) : data(new char[strlen(other.data) + 1]) {
strcpy(data, other.data);
++copy_count;
std::cout << "Copy construction\n";
}
Str operator+(const Str& other) const {
size_t len = strlen(data) + strlen(other.data) + 1;
char* new_data = new char[len];
strcpy(new_data, data);
strcat(new_data, other.data);
Str result(new_data);
delete[] new_data; // Temporary allocation for + result
return result;
}
~Str() {
delete[] data;
std::cout << "Destruction\n";
}
};
int Str::construct_count = 0;
int Str::copy_count = 0;
Str make_concat() {
Str temp1("hello");
Str temp2(" world");
return temp1 + temp2; // Returns temporary from operator+
}
int main() {
Str result = make_concat();
std::cout << "Final counts - Constructs: " << Str::construct_count
<< ", Copies: " << Str::copy_count << "\n";
return 0;
}
In this code, make_concat constructs two temporary Str objects, concatenates them to produce another temporary via operator+, and returns that result. With copy elision enabled (the default in modern compilers), the returned temporary is constructed directly into result, yielding output such as:
Construction
Construction
Construction
Destruction
Destruction
Destruction
Final counts - Constructs: 3, Copies: 0
Construction
Construction
Construction
Destruction
Destruction
Destruction
Final counts - Constructs: 3, Copies: 0
Here, the three constructions correspond to temp1, temp2, and the direct construction of result (eliding the copy from the + temporary); the three destructions follow their scopes. Without elision (e.g., compiled with GCC's -fno-elide-constructors flag), an additional copy construction occurs for the returned temporary into result, producing:
Construction
Construction
Construction
Copy construction
Destruction
Destruction
Destruction
Destruction
Final counts - Constructs: 3, Copies: 1
Construction
Construction
Construction
Copy construction
Destruction
Destruction
Destruction
Destruction
Final counts - Constructs: 3, Copies: 1
The extra copy and destruction reflect the unelided temporary.[9]
This case achieves zero copies for the return in eliding mode and is compatible with C++98, where such elision was permitted (though optional) to optimize performance even in early standards. In C++17 and later, elision becomes guaranteed for prvalue returns like this temporary, ensuring consistent zero-copy behavior across compliant compilers.[9]
Exception Case
In exception handling, copy elision can occur when an exception object is thrown and subsequently caught by value, allowing the compiler to omit the copy construction of the catch parameter from the exception object.[1] This optimization treats the catch clause's parameter as an alias for the exception object itself, provided the types match (ignoring cv-qualifiers) and no observable behavior changes except for omitted constructor and destructor calls.[1]
Consider the following example, where a class MyException logs its constructors and destructor to illustrate the elision:
cpp
#include <iostream>
class MyException {
public:
MyException(const std::string& msg) : message(msg) {
std::cout << "Constructor called with: " << message << std::endl;
}
MyException(const MyException& other) : message(other.message) {
std::cout << "Copy constructor called" << std::endl;
}
std::string getMessage() const { return message; }
~MyException() {
std::cout << "Destructor called" << std::endl;
}
private:
std::string message;
};
void riskyFunction() {
try {
throw MyException("Error occurred!");
} catch (MyException e) {
std::cout << "Caught: " << e.getMessage() << std::endl;
}
}
#include <iostream>
class MyException {
public:
MyException(const std::string& msg) : message(msg) {
std::cout << "Constructor called with: " << message << std::endl;
}
MyException(const MyException& other) : message(other.message) {
std::cout << "Copy constructor called" << std::endl;
}
std::string getMessage() const { return message; }
~MyException() {
std::cout << "Destructor called" << std::endl;
}
private:
std::string message;
};
void riskyFunction() {
try {
throw MyException("Error occurred!");
} catch (MyException e) {
std::cout << "Caught: " << e.getMessage() << std::endl;
}
}
When riskyFunction is executed with copy elision enabled (as permitted by the C++ standard), the output typically shows only one constructor call for the temporary exception object, which is directly used as the exception object and aliased by the catch parameter e, followed by a single destructor call at the end of the scope.[1] Without elision, an additional copy constructor would be invoked to initialize e from the exception object, and an extra destructor would run for the temporary, demonstrating the performance benefit of omitting these operations.[1]
If the caught exception is rethrown using throw;, further copy elision applies: the rethrow expression elides any copy or move from the current exception object to a new one, constructing the new exception directly in the caller's context.[1] In a non-elided scenario for rethrow (e.g., in older compilers or specific configurations), an extra copy constructor and destructor pair would appear, highlighting the overhead avoided by this optimization.
This case underscores the value of copy elision in exception paths, where avoiding copies of potentially large or expensive-to-construct objects can improve efficiency, particularly in performance-sensitive applications involving frequent error conditions.[29]
Initialization Case
In the initialization case of copy elision, a class object is copy-initialized from a prvalue (pure rvalue) expression of the same type, allowing the compiler to construct the object directly in the target storage without creating an intermediate temporary or invoking the copy or move constructor.[1] This optimization applies when the initializer is a nameless temporary, such as in variable declarations or constructor calls where a temporary is passed by value.[4]
Consider the following example, where a class Foo tracks constructor invocations via output:
cpp
#include <iostream>
class Foo {
public:
Foo() { std::cout << "default constructor\n"; }
Foo(const Foo&) { std::cout << "copy constructor\n"; }
Foo(Foo&&) { std::cout << "move constructor\n"; }
~Foo() { std::cout << "destructor\n"; }
};
int main() {
Foo f = Foo(); // Copy-initialization from a prvalue
}
#include <iostream>
class Foo {
public:
Foo() { std::cout << "default constructor\n"; }
Foo(const Foo&) { std::cout << "copy constructor\n"; }
Foo(Foo&&) { std::cout << "move constructor\n"; }
~Foo() { std::cout << "destructor\n"; }
};
int main() {
Foo f = Foo(); // Copy-initialization from a prvalue
}
Without elision, this would construct a temporary Foo object using the default constructor, then invoke the copy or move constructor to initialize f from that temporary, followed by two destructors. With copy elision, only the default constructor is called once for f directly, and one destructor executes at the end.[1] The same principle extends to constructor arguments passed by value; for instance, in void func(Foo param); func(Foo());, the prvalue Foo() is constructed directly into param's storage, eliding any copy or move into the parameter.[4]
Prior to C++17, this elision was permitted but not guaranteed, potentially leading to a copy or move in some compiler implementations.[1] C++11 introduced uniform initialization syntax (using braces {}), which facilitates similar elisions and often reduces temporaries compared to pre-C++11 parenthesized styles; for example, Foo f{Foo()}; behaves equivalently to the copy-initialization form but avoids implicit type conversions that might create extra objects in older code.
This form of copy elision is common in RAII (Resource Acquisition Is Initialization) patterns, where objects manage resources via constructors; direct construction ensures efficient resource acquisition without unnecessary copies, preserving exception safety and performance.[1]