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
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 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 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 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.