Triggered by this answer I was reading in the core guidelines:
C.45: Don’t define a default constructor that only initializes data members; use in-class memb
I think that it's important to assume that C.45 refers to constants (example and enforcement):
Example, bad
class X1 { // BAD: doesn't use member initializers string s; int i; public: X1() :s{"default"}, i{1} { } // ... };
Example
class X2 { string s = "default"; int i = 1; public: // use compiler-generated default constructor // ... };
Enforcement
(Simple) A default constructor should do more than just initialize member variables with constants.
With that in mind, it's easier to justify (via C.48) why we should prefer in-class initializers to member initializers in constructors for constants:
C.48: Prefer in-class initializers to member initializers in constructors for constant initializers
Reason
Makes it explicit that the same value is expected to be used in all constructors. Avoids repetition. Avoids maintenance problems. It leads to the shortest and most efficient code.
Example, bad
class X { // BAD int i; string s; int j; public: X() :i{666}, s{"qqq"} { } // j is uninitialized X(int ii) :i{ii} {} // s is "" and j is uninitialized // ... };
How would a maintainer know whether j was deliberately uninitialized (probably a poor idea anyway) and whether it was intentional to give s the default value "" in one case and qqq in another (almost certainly a bug)? The problem with j (forgetting to initialize a member) often happens when a new member is added to an existing class.
Example
class X2 { int i {666}; string s {"qqq"}; int j {0}; public: X2() = default; // all members are initialized to their defaults X2(int ii) :i{ii} {} // s and j initialized to their defaults // ... };
Alternative: We can get part of the benefits from default arguments to constructors, and that is not uncommon in older code. However, that is less explicit, causes more arguments to be passed, and is repetitive when there is more than one constructor:
class X3 { // BAD: inexplicit, argument passing overhead int i; string s; int j; public: X3(int ii = 666, const string& ss = "qqq", int jj = 0) :i{ii}, s{ss}, j{jj} { } // all members are initialized to their defaults // ... };
Enforcement
(Simple) Every constructor should initialize every member variable (either explicitly, via a delegating ctor call or via default
construction). (Simple) Default arguments to constructors suggest an in-class initializer may be more appropriate.
A default
ed constructor should have the same generated assembly as the equivalent initializer constructor provided that the author includes the correct constexpr
and noexcept
statuses.
I suspect the "can be more efficient" is referring to the fact that, in general, it will generate more optimal code than the equivalent developer-authored one that misses opportunities such as inline
, constexpr
, and noexcept
.
An important feature that default
ed constructors perform is that they interpret and deduce the correct status for both constexpr
and noexcept
This is something that many C++ developers do not specify, or may not specify correctly. Since Core Guidelines targets both new and old C++ developers, this is likely why the "optimization" is being mentioned.
The constexpr
and noexcept
statuses may affect code generation in different ways:
constexpr
constructors ensure that invocations of a constructor from values yielded from constant expressions will also yield a constant expression. This can allow things like static
values that are not constant to not actually require a constructor invocation (e.g. no static initialize overhead or locking required). Note: this works for types that are not, themselves, able to exist in a constexpr
context -- as long as the constexpr
ness of the constructor is well-formed.
noexcept
may generate better assembly of consuming code since the compiler may assume that no exceptions may occur (and thus no stack-unwinding code is necessary). Additionally, utilities such as templates that check for std::is_nothrow_constructible...
may generate more optimal code paths.
Outside of that, default
ed constructors defined in the class-body also make their definitions visible to the caller -- which allows for better inlining (which, again, may otherwise be a missed-opportunity for an optimization).
The examples in the Core Guidelines don't demonstrate these optimizations very well. However, consider the following example, which illustrates a realistic example that can benefit from default
ing:
class Foo {
int a;
std::unique_ptr<int> b;
public:
Foo() : a{42}, b{nullptr}{}
};
In this example, the following are true:
Foo{}
is not a constant expressionFoo{}
is not noexcept
Contrast this to:
class Foo {
int a = 42;
std::unique_ptr<int> b = nullptr;
public:
Foo() = default;
};
On the surface, this appears to be the same. But suddenly, the following now changes:
Foo{}
is constexpr
, because std::unique_ptr
's std::nullptr_t constructor is constexpr
(even though std::unique_ptr
cannot be used in a full constant expression)Foo{}
is a noexcept
expressionYou can compare the generated assembly with this Live Example. Note that the default
case does not require any instructions to initialize foo
; instead it simply assigns the values as constants through compiler directive (even though the value is not constant).
Of course, this could also be written:
class Foo {
int a;
std::unique_ptr<int> b;
public:
constexpr Foo() noexcept :a{42}, b{nullptr};
};
However, this requires prior knowledge that Foo
is able to be both constexpr
and noexcept
. Getting this wrong can lead to problems. Worse yet, as code evolves over time, the constexpr
/noexcept
state may become incorrect -- and this is something that default
ing the constructor would have caught.
Using default
also has the added benefit that, as code evolves, it may add constexpr
/noexcept
where it becomes possible -- such as when the standard library adds more constexpr
support. This last point is something that would otherwise be a manual process every time code changes for the author.
If you take away the use of in-class member initializers, then there is one last worthwhile point mentioning: there is no way in code to achieve triviality unless it gets compiler-generated (such as through default
ed constructors).
class Bar {
int a;
public:
Bar() = default; // Bar{} is trivial!
};
Triviality offers a whole different direction on potential optimizations, since a trivial default-constructor requires no action on the compiler. This allows the compiler to omit any Bar{}
entirely if it sees that the object is later overwritten.