Wednesday, July 1, 2026

Stop Using Empty Braces {} for C++ Constructors

Empty constructors can disable useful compiler optimizations.

// Avoid this
MyClass() {}

// Prefer this
MyClass() = default;

1. The Performance Secret: "Trivial" Types

With {} (the slow way), the constructor becomes user-provided, making the type non-trivially default constructible. That prevents standard library implementations from using the highly optimized code paths available for trivial types.

With "= default" (the fast way), the constructor remains compiler-generated, allowing the type to stay trivially default constructible when all its members are trivial. This enables the standard library and compiler to use optimized code paths that are unavailable for user-provided constructors.

In the following example, the compiler generates machine instructions for A but not for B. From the language's perspective "user-provided" and "does nothing" are different concepts:

// C++ code
struct A { A() {} };
struct B { B() = default;};
int main() {
  A a;
  B b;
}

//Assembly code (generated with Compiler Explorer, x86-64 clang (trunk), no optimization:
main:
  sub rsp, 16
  lea rdi, [rbp - 1]
  call A::A() [base object constructor]
  xor eax, eax
  add rsp, 16
  pop rbp
  ret

A::A() [base object constructor]:
  push rbp
  mov rbp, rsp
  mov qword ptr [rbp - 8], rdi
  pop rbp
  ret

Note that if you use the optimization flag -O1, the code reduces to its optimal form:

main:
  xor eax, eax
  ret

This is a good illustration of a broader point: at -O0, C++ codegen tends to reflect the syntax of your code fairly literally (useful for debugging, every statement maps predictably to instructions). At -O1 and above, codegen instead reflects the observable semantics and "construct two empty objects nobody uses" has no observable semantics at all.

2. Guarding Your Move Semantics

Writing an empty destructor ~MyClass() {} triggers the Rule of Five in a bad way. The compiler assumes that because you wrote a custom destructor, you are managing resources manually. It automatically deletes your implicit move constructor and move assignment operator. By trying to be clean with {}, you accidentally cripple your class's ability to be efficiently moved. Using "= default" keeps your move semantics intact.

3. Why Can't the Compiler Just Optimize {}?

It isn't a lack of compiler intelligence; it is a legal restriction. The C++ Standard explicitly states that writing {} makes a constructor user-provided. Changing this rule would break the legacy code. For example, if the standard suddenly declared that empty braces implied "trivial," code that intentionally uses {} to block unsafe memcpy operations (like cryptography classes) would instantly become vulnerable. Furthermore, if you had an empty #ifdef DEBUG block inside your constructor, your class would dangerously flip between being non-trivial in Debug mode and trivial in Release mode.

The C++ committee gave us "= default" in C++11 as an explicit toggle. {} tells the compiler, "Hands off, I'm taking control." = default tells the compiler, "I want standard behavior, optimize this as much as you can." Always give the compiler the green light.

No comments:

Post a Comment