In C++17, the standard introduced guaranteed copy elision (mandatory copy elision), which fundamentally changes how prvalues (pure rvalues) are materialized. When a prvalue of class type initializes an object of the same type—such as when returning from a function by value or passing a temporary to a function—the object is constructed directly in the destination storage. Consequently, the copy constructor or move constructor is not invoked, and importantly, neither their accessibility (public vs. private) nor their mere existence (provided the class is complete and destructible) is required for the operation to be well-formed. This contrasts sharply with earlier standards where elision was merely an optional optimization that still mandated accessible and present constructors for compilation.
struct Immovable { Immovable() = default; Immovable(const Immovable&) = delete; Immovable(Immovable&&) = delete; }; Immovable factory() { return Immovable{}; // OK in C++17: no move/copy invoked } void consume(Immovable x); // Parameter initialized directly from prvalue
Our team was building a kernel-mode driver where resource handles wrapping hardware contexts could not be duplicated or relocated in memory due to registered kernel addresses. We needed a factory function to produce these handles by value for RAII management, but the handles explicitly deleted both copy and move constructors to prevent accidental invalidation of kernel mappings. Prior to C++17, this design was incompatible with returning by value because even with NRVO, the compiler conceptually required the move constructor to be accessible, resulting in compilation errors.
Solution 1: Heap allocation via std::unique_ptr
We considered wrapping the handle in a std::unique_ptr, allowing the pointer to be moved while the underlying object remained pinned. This approach provided safety and functioned in C++14.
Pros: Standard memory management, prevents leaks, widely supported across legacy codebases.
Cons: Introduces dynamic allocation overhead and pointer indirection, which is prohibitive in kernel contexts where deterministic low latency is required; also fragments the CPU cache and requires exception handling considerations for allocation failure.
Solution 2: Out-parameter initialization
Passing a reference to a caller-allocated object into the factory to be initialized in place.
Pros: Zero-copy guarantee regardless of C++ standard version; no heap allocation; compatible with immovable types.
Cons: Destroys the fluent API style (auto h = create(); becomes Handle h; create(h);); increases risk of use-before-initialization and composes poorly with standard algorithms and range-based for loops.
Solution 3: Leverage C++17 guaranteed copy elision
We refactored the factory to return the immovable type by value, relying on mandatory elision to construct the prvalue directly into the caller’s storage.
Pros: Eliminates heap usage; preserves value semantics; enforces zero-cost abstraction at compile time; move/copy constructors need not exist or be accessible.
Cons: Applies strictly to pure rvalues (cannot return existing named variables); requires compiler with C++17 support; subtle differences in exception handling during construction must be understood.
We selected Solution 3 because the factory produced fresh temporaries that were pure prvalues, perfectly matching the guaranteed elision scenario. This allowed the handles to remain strictly immovable while maintaining ergonomic value semantics and compatibility with auto declarations.
The driver shipped with microsecond-scale initialization for thousands of concurrent connections. Assembly inspection confirmed the handle was constructed directly in the caller’s stack frame without any relocation or copy code. The type system enforced resource safety by construction, and we eliminated heap contention entirely from the hot path.
Does guaranteed copy elision apply to named return values (lvalues) inside the function, or is it strictly limited to prvalues?
Guaranteed copy elision applies exclusively to prvalues (pure rvalues), such as temporaries created in the return statement without a name. Named Return Value Optimization (NRVO) remains an optional compiler optimization; while widely implemented, it does not provide the same guarantees regarding constructor accessibility or side effects. If a candidate attempts to return a named local variable and assumes it will trigger guaranteed elision even if the move constructor is deleted, the program will be ill-formed because named variables are lvalues and require move/copy operations unless the compiler applies optional NRVO, which is not mandated.
Can a class with explicitly deleted copy and move constructors be returned by value from a function under guaranteed copy elision rules?
Yes. In C++17, if the returned expression is a prvalue (e.g., return MyClass{};), the copy and move constructors are never considered for the initialization. Because the object is constructed directly in the caller’s storage, the deleted constructors are not odr-used and do not cause compilation errors. However, attempting to return a named variable of such a type will fail, as that operation conceptually requires moving the lvalue into the return slot, which would invoke the deleted move constructor and result in an ill-formed program.
How does guaranteed copy elision interact with exception safety, specifically regarding the lifetime of the prvalue temporary during stack unwinding?
Under guaranteed copy elision, there is no separate temporary object created before the target object’s lifetime begins. The prvalue is materialized directly at its final destination. Consequently, if an exception occurs during the construction of the prvalue, the stack unwinding mechanism does not encounter a separate temporary that requires destruction; instead, it sees the partially constructed destination object. This means that from the perspective of the caller, the object either exists fully constructed or not at all, simplifying exception safety guarantees and ensuring that no double-destruction or resource leak occurs due to an abandoned temporary during exception handling before the destination object’s lifetime officially starts.