显式默认拷贝构造函数生成代码优于手写等价实现的原因探究
Great catch! The difference you've observed between explicitly defaulted copy constructors (=default) and hand-written implementations that look identical is entirely intentional, rooted in C++ standard rules around trivial special member functions and compiler optimizations. Both Clang and GCC are following the standard here—let's break this down.
1. The Key Distinction: Triviality of the Copy Constructor
First, the C++ standard defines a trivial copy constructor (C++11 and later) with strict criteria:
A copy constructor is trivial if and only if:
- It is not user-provided (i.e., it's either implicitly generated or explicitly defaulted with
=default)- The class has no virtual functions or virtual base classes
- All direct base classes have trivial copy constructors
- All non-static data members have trivial copy constructors
A hand-written copy constructor—even if it does exactly what the default implementation would do—counts as user-provided, which immediately disqualifies it from being trivial.
Why does triviality matter? Compilers get to treat trivial types and their special members differently:
- For trivial copy constructors, the compiler can replace the constructor call with a raw memory copy (like
memcpy)—this is far faster than even a simple member-wise copy, though for a singleintmember the difference is small, it's still visible in generated assembly. - Trivial constructors also enable more aggressive return value optimizations (RVO/NRVO). Compilers can skip copying return values entirely by constructing the object directly in the caller's stack frame, which is often not applied as reliably to non-trivial constructors (especially in debug builds).
2. C++ Standard References (C++20)
Let's anchor this to the standard:
- [class.copy.ctor] §12: Explicitly states that a user-provided copy constructor cannot be trivial. It also outlines the full criteria for trivial copy constructors.
- [class.trivial] §1: Defines trivial types as those with trivial special member functions, noting that these types can be treated as raw memory by the compiler.
- [class.copy.elision] §1: While copy elision is allowed for non-trivial constructors, the standard explicitly permits compilers to perform more aggressive optimizations for trivial cases, including skipping copy operations even when they have side effects (though your test class has no side effects, this still affects code generation).
3. How This Applies to Your average() Functions
Suppose your test code looks something like this:
struct Num { int val; // Explicitly defaulted copy ctor (trivial) Num(const Num&) = default; // OR hand-written copy ctor (non-trivial) // Num(const Num& other) : val(other.val) {} Num operator+(const Num& rhs) const { return Num{val + rhs.val}; } }; Num average(const Num& a, const Num& b) { Num sum = a + b; sum.val /= 2; return sum; }
For the =default version:
- The compiler recognizes the copy constructor as trivial, so it can use NRVO to construct
sumdirectly in the caller's stack frame—no copy operation is generated when returningsum. - When passing
Numobjects as parameters, the compiler can usememcpyinstead of calling the copy constructor.
For the hand-written version:
- Even though the logic is identical, the copy constructor is non-trivial. The compiler may generate explicit copy constructor calls (especially in debug builds), and NRVO might be less reliably applied (though optimizations like
-O2may still eliminate the copy, the assembly will differ from the trivial case).
4. Final Takeaways
- Explicitly defaulted copy constructors retain triviality (if they meet the criteria), unlocking more aggressive compiler optimizations.
- Hand-written copy constructors, even when logically identical, are marked as user-provided and lose triviality, leading to different code generation.
- This behavior is fully compliant with the C++ standard—compilers are following the rules for trivial special members to optimize where possible.
内容的提问来源于stack exchange,提问作者Chris Uzdavinis

