13

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 member initializers instead

The reasoning given is

Reason

Using in-class member initializers lets the compiler generate the function for you. The compiler-generated function can be more efficient.

Note that this is specifically about a default constructor that does nothing but initialize the members and the guideline suggests that one should not write such a constructor.

The "bad" example is:

Example, bad

class X1 { // BAD: doesn't use member initializers
    string s;
    int i;
public:
    X1() :s{"default"}, i{1} { }
    // ...
};

The "good" example is using in-class member initializers and no user declared constructor:

Example

class X2 {
    string s = "default";
    int i = 1;
public:
    // use compiler-generated default constructor
    // ...
};

What can the compiler generated constructor do more efficient than the user-provided one in that particular example (or in any other example)?

Is the initializer list not giving the same opportunities for optimization as in-class initializers?

463035818_is_not_an_ai
  • 109,796
  • 11
  • 89
  • 185
  • 2
    @P.W it is closely related but not a duplicate imho. – 463035818_is_not_an_ai May 23 '19 at 09:54
  • @P.W on a second thought maybe it is a perfect duplicate. Have to think about it and study a bit more ;) feel free to flag – 463035818_is_not_an_ai May 23 '19 at 09:58
  • 1
    The actual answer is https://stackoverflow.com/a/4417899/10749452, isn't it? – Benjamin Bihler May 23 '19 at 10:55
  • 1
    @BenjaminBihler In this question the class has NSDMIs which makes the constructor non-trivial , so the considerations in that answer don't apply to this question – M.M Jan 20 '21 at 21:17
  • Note that the "core guidelines" are just stylistic opinions of a small group of people , so there is an opinion-based element to this question – M.M Jan 20 '21 at 21:18
  • 1
    @M.M. well, "The compiler-generated function can be more efficient." is a statement that can be right or wrong. It can be "right" in a very general sense that anything can be more efficient than something else in certain circumstances, but I don't think this is what they meant when writing the Reason. Note that I was fine with the close as duplicate, though I am still a little puzzled what they meant when writing "can be more efficient" – 463035818_is_not_an_ai Jan 20 '21 at 22:58

2 Answers2

11

Short Answer

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

Long Answer

An important feature that defaulted 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 constexprness 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, defaulted 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 defaulting:

class Foo {
    int a;
    std::unique_ptr<int> b;
public:
    Foo() : a{42}, b{nullptr}{}
};

In this example, the following are true:

  • A construction of Foo{} is not a constant expression
  • Construction of Foo{} 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 expression

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


Triviality

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

Human-Compiler
  • 11,022
  • 1
  • 32
  • 59
  • Your idea of _trviality_ ran through my mind too, but I now think it's not true. [I read this to mean that both forms of initialization are non-trivial](http://eel.is/c++draft/class.ctor#class.default.ctor-3.2). Some tests with `std::is_trivial` seem to confirm. (Perhaps I'm wrong, of course) – Drew Dormann Jan 23 '21 at 04:45
  • @DrewDormann You're correct -- members with initializers are not trivial, which is why my second set of examples do not use an initializer. However, rereading the question I realize it explicitly mentions initializers. Whoops! – Human-Compiler Jan 23 '21 at 04:48
  • There are also some semantic differences: a class type with an explicitly-defaulted constructor (defaulted at its first declaration) _can_ be an aggregate (before C++20; given that it fulfills the other restrictions for a class type for be an aggregate), whereas one with a _user-provided_ constructor cannot. – dfrib Jan 25 '21 at 14:26
  • @dfrib Although that is true, I don't believe the state of being an aggregate would actually provide any better efficiency/optimizations than the equivalent `default`ed constructor would, which is what the question is ultimately about. – Human-Compiler Jan 25 '21 at 15:11
  • 1
    @Human-Compiler this is certainly in the domain of speculation (/negligible effect), but braced-initialization of an aggregate class means value-initialization of its members, whereas braced-initialization of a non-aggregate class means default-initialization of its members (assuming the latter has an empty user-provided ctor, or an out-of-class explicitly-defaulted ctor). For a contrived class with a data member that is, say, a huge array of a fundamental type, say `struct S { int arr_[1000000]; /* .... */ };`, braced-initialization for `S` would mean either zero-initialization of ... – dfrib Jan 25 '21 at 15:21
  • 1
    ... the data member `arr_` or effectively _no_ initialization of the data member `arr_` (indeterminate values) depending on whether `S` is an aggregate class or not, respectively. The latter _could_ arguably (in this domain of speculation) be more efficient, whilst opening up a door for UB, as well as a door for implementors to assume we have no UB, and possible perform subsequent optimizations that would not be possible were the data member to be zero-initialized. (Or it other words, in could allow unintended mistakes that could open up for dangerous optimizations). – dfrib Jan 25 '21 at 15:22
1

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.

Jose
  • 3,306
  • 1
  • 17
  • 22
  • 2
    you make a really good point. As they write also in C48 "Avoids maintenance problems. It leads to the shortest and most efficient code." this suggests that also in C45 they use "effiecient" as in "easier to maintain code" rather than "more opportunities for the compiler.". Note that this doesnt answer "Is the initializer list not giving the same opportunities for optimization as in-class initializers?", ie is there a difference with respect to compiler optimization? I am starting to belive no. – 463035818_is_not_an_ai Jan 21 '21 at 11:05
  • @largest_prime_is_463035818 Neither I am. I do think that "optimization" here indicates "less error-prone". C.48 says that it's more efficient, but the [example](https://github.com/isocpp/CppCoreGuidelines/blob/036324/CppCoreGuidelines.md#example-bad-35) and the [alternative](https://github.com/isocpp/CppCoreGuidelines/blob/036324/CppCoreGuidelines.md#example-89) refer to developer's errors instead of optimizations. – Jose Jan 21 '21 at 16:45