Defaulted vs Implicit Constructors in C++

In C++, both implicitly generated constructors and explicitly defaulted constructors (= default) allow the compiler to synthesize baseline member initialization logic. While they often produce identical machine code, they differ in critical ways regarding access control, triviality, user-declared status, aggregate classification, and definition placement. Understanding these distinctions is essential for designing robust types that interact correctly with type traits, overload resolution, and initialization rules.

User-Declared Status and Other Special Member Functions

An implicitly generated default constructor only exists if no other constructors are declared by the user. If you declare any parameterized constructor, the compiler suppresses the implicit default constructor entirely.

An explicitly defaulted constructor (T() = default;) is considered a user-declared constructor. Declaring it explicitly forces the constructor into existence even when parameterized constructors are present. However, because it is user-declared, it impacts how the compiler interprets the class:

  • In standards prior to C++20, declaring any constructor—even = default—disqualified a class from being an aggregate type. In C++20 and later, an explicitly defaulted constructor is permitted in an aggregate, provided it is not user-provided.
  • An explicitly defaulted constructor communicates design intent clearly, assuring both the compiler and maintainers that default construction is deliberately supported.

Access Control Customization

Implicitly generated constructors are always generated with public access. The compiler cannot automatically place an implicit constructor in a protected or private scope.

An explicitly defaulted constructor can be placed under any access specifier:

  • protected: Useful for base classes that should not be directly instantiated on their own, but need compiler-generated construction logic available to derived classes.
  • private: Useful for patterns such as singletons or factory-based creation methods where external instantiation must be prohibited while retaining compiler-generated initialization.

In-Class vs. Out-of-Line Definitions and Triviality

An implicitly generated constructor is always defined inline where the class definition resides. Its triviality depends strictly on whether the class's base classes and non-static data members have trivial default constructors.

An explicitly defaulted constructor can be defaulted either inside the class definition or out-of-line:

  • Defaulted inside the class definition (T() = default;): It is treated as non-user-provided. If the members and base classes allow it, the constructor remains trivial.
  • Defaulted out-of-line (T::T() = default;): It is treated as user-provided. A user-provided constructor is never trivial, even if it uses = default. This alters traits like std::is_trivial and std::is_trivially_default_constructible, affecting whether objects of the type can be manipulated via raw memory operations such as std::memcpy.
  • Header decoupling: Out-of-line defaulting allows you to declare a defaulted constructor in a header file and define it in an implementation file (.cpp). This is especially valuable when managing types that hold incomplete types via std::unique_ptr (Pimpl idiom), where the destructor or constructor must be instantiated only where the complete type definition is visible.

Exception Specifications and Attributes

An implicitly generated constructor automatically deduces its noexcept status based on whether all base and member operations invoked are non-throwing. It also automatically deduces constexpr readiness if the operations satisfy constexpr requirements.

With explicit defaulting:

  • You can explicitly attach qualifiers such as constexpr or explicit exception specifications (e.g., constexpr T() noexcept = default;).
  • If you supply an explicit noexcept(true) specification on an explicitly defaulted constructor and a member initialization could actually throw, the compiler will flag this mismatch as a compile-time error, giving you a mechanism to strictly enforce compile-time contracts.