What Is Default Member Initialization?

A struct definition normally says only what a member is. Default member initialization lets it also say what the member starts as: you attach an initializer to the member inside the type definition, and every object of that type picks the value up unless the code creating the object overrides it.

An initializer written on a non-static member like this is called a default member initializer, and the act of writing one is sometimes called non-static member initialization.

struct BakeSetting
{
    int trayCount;       // no default member initializer
    int minutes {};      // default member initializer is an empty pair of braces
    int celsius { 180 }; // default member initializer with an explicit value
};

The type now carries three different promises. trayCount promises nothing. minutes promises to be value-initialized, which for an int means 0. celsius promises to be 180. Nothing has been created yet, and no object has been written out, but the defaults are already decided.

Related Content
Members marked static are initialized by a different mechanism. We cover those in the Static member variables lesson.

Four Ways a Member Ends Up With a Value

Rather than memorising rules one at a time, it helps to see the whole decision at once. When an object is created, the compiler asks two questions about each member: did the initializer list supply a value for it, and does the member have a default member initializer? Those two questions produce four outcomes.

How the object is defined Member has a default member initializer Member has none
Braced list supplies a value for this member the supplied value wins the supplied value wins
Braced list runs out before this member the default member initializer is used the member is value-initialized
Empty braces {} the default member initializer is used the member is value-initialized
No braces at all the default member initializer is used the member is left uninitialized

Three columns of that table are safe. One cell is not, and the whole rest of this lesson is really about avoiding it.

Two details are worth pinning down before we test the table. First, a supplied value always beats a default member initializer, so a default is a fallback rather than a constraint. Second, members are filled in top to bottom in declaration order, so "the list runs out" always means "runs out at some member and every member below it".

Testing the Table Against a Real Struct

The BakeSetting struct above covers both member columns, so three object definitions are enough to exercise the first three rows.

#include <iostream>

struct BakeSetting
{
    int trayCount;       // no default member initializer
    int minutes {};      // value-initialized by default
    int celsius { 180 }; // explicit default value
};

int main()
{
    BakeSetting fullList { 3, 40, 220 };
    BakeSetting shortList { 3 };
    BakeSetting emptyList {};

    std::cout << "fullList:  " << fullList.trayCount << ' ' << fullList.minutes << ' ' << fullList.celsius << '\n';
    std::cout << "shortList: " << shortList.trayCount << ' ' << shortList.minutes << ' ' << shortList.celsius << '\n';
    std::cout << "emptyList: " << emptyList.trayCount << ' ' << emptyList.minutes << ' ' << emptyList.celsius << '\n';

    return 0;
}
fullList:  3 40 220
shortList: 3 0 180
emptyList: 0 0 180

Read each row against the table. fullList supplies all three values, so no default is consulted at all and celsius prints 220 rather than 180. shortList supplies only trayCount; the list runs out, so minutes and celsius fall back to their default member initializers. emptyList supplies nothing, so minutes and celsius use their defaults while trayCount, which has no default, is value-initialized to 0.

Notice that celsius printed 180 twice without anyone writing 180 inside main(). That is the point of the feature: the type knows its own sensible starting state.

Key Concept
With default member initializers, a struct can put itself into a usable state without the calling code having to know or repeat what "usable" means.

The Dangerous Cell: No Braces and No Default

The fourth row of the table is the one that produces bugs. Dropping the braces entirely is default initialization, and default initialization does not touch members that have no default member initializer.

#include <iostream>

struct BakeSetting
{
    int trayCount;       // no default member initializer
    int minutes {};      // value-initialized by default
    int celsius { 180 }; // explicit default value
};

int main()
{
    BakeSetting noBraces;

    std::cout << "minutes: " << noBraces.minutes << '\n';
    std::cout << "celsius: " << noBraces.celsius << '\n';

    return 0;
}
minutes: 0
celsius: 180

The two members with defaults behaved exactly as before. noBraces.trayCount is deliberately missing from the output, because it holds whatever bit pattern happened to be in that memory. Its value is indeterminate, and reading it is undefined behaviour, so there is no output we could honestly print for it.

The fix is not to remember which objects need braces. The fix is to remove the fourth row from your program entirely by giving every member a default:

#include <iostream>

struct BakeSetting
{
    int trayCount { 1 };
    int minutes { 25 };
    int celsius { 180 };
};

int main()
{
    BakeSetting noBraces;
    BakeSetting emptyList {};
    BakeSetting quickBatch { 2, 12 };

    std::cout << "noBraces:   " << noBraces.trayCount << ' ' << noBraces.minutes << ' ' << noBraces.celsius << '\n';
    std::cout << "emptyList:  " << emptyList.trayCount << ' ' << emptyList.minutes << ' ' << emptyList.celsius << '\n';
    std::cout << "quickBatch: " << quickBatch.trayCount << ' ' << quickBatch.minutes << ' ' << quickBatch.celsius << '\n';

    return 0;
}
noBraces:   1 25 180
emptyList:  1 25 180
quickBatch: 2 12 180

Every object is fully initialized now, whichever form was used. A member with no default is a trap waiting for the one call site that forgets its braces, and adding {} or { someValue } to the member costs nothing.

Best Practice
Give every member a default member initializer, either an explicit value or an empty pair of braces. Then no object definition can leave a member uninitialized.

Empty Braces or No Braces?

Look at the first two objects in the program above. noBraces is default initialized and emptyList is value initialized, and the printed results are identical. So does the choice matter?

It matters as insurance rather than as behaviour. The two forms agree only for as long as every member keeps its default. The day a member is added to BakeSetting without one, emptyList still gets a zeroed member while noBraces silently gains an indeterminate one, and nothing at the call site changes to warn you. Empty braces stay correct through that edit; no braces does not.

The second reason is consistency. You already write int trayCount {}; rather than int trayCount; for fundamental types, and using the same shape for struct objects means there is one habit to hold rather than two.

Best Practice
Prefer value initialization (BakeSetting setting {};) over default initialization (BakeSetting setting;) for aggregates.

You will still meet plenty of code that omits the braces. Value initialization only arrived in C++11, and for some non-aggregate types default initialization can genuinely be the cheaper option, a case we return to when we cover default constructors. Treat the braces as a strong default rather than an absolute.

Declaration Order Is Part of the Behaviour

Because members are initialized top to bottom, a default member initializer is allowed to depend on members declared above it.

#include <iostream>

struct TrayLayout
{
    int rows { 3 };
    int columns { 4 };
    int slots { rows * columns };
};

int main()
{
    TrayLayout standard {};
    TrayLayout narrow { 2, 5 };

    std::cout << "standard slots: " << standard.slots << '\n';
    std::cout << "narrow slots:   " << narrow.slots << '\n';

    return 0;
}
standard slots: 12
narrow slots:   10

narrow supplies 2 and 5, the list then runs out, and slots falls back to its default member initializer, which is evaluated with the values this particular object already has. The default is not a constant baked into the type; it is an expression run once per object.

Warning
This only works downwards. If slots were declared first, its initializer would read rows and columns before they were initialized, which is undefined behaviour. Keep derived members below the members they derive from, or avoid the dependency altogether.

Which Initializer Forms Are Allowed

A default member initializer may be written with braces or with an equals sign, so int celsius { 180 }; and int celsius = 180; both work. Parentheses do not, and the resulting error is a confusing one because the compiler reads the line as a function declaration instead. The next program is broken on purpose to show what that looks like:

#include <iostream>

struct BakeSetting
{
    int trayCount { 1 };
    int minutes { 25 };
    int celsius(180); // will not compile
};

int main()
{
    BakeSetting standard {};
    std::cout << standard.celsius << '\n';

    return 0;
}
s.cpp:7:17: error: expected identifier before numeric constant
    7 |     int celsius(180); // will not compile
      |                 ^~~
s.cpp:7:17: error: expected ',' or '...' before numeric constant
s.cpp: In function 'int main()':
s.cpp:13:15: error: invalid use of non-static member function 'int BakeSetting::celsius(int)'
   13 |     std::cout << standard.celsius << '\n';
      |     ~~~~~~~~~~^~~~~~~~~~~~~~~~~~~

The phrase non-static member function in that message is the giveaway: the compiler decided celsius was a function taking an int. Braces avoid the ambiguity, which is one more reason to use them everywhere.

Adding default member initializers does not stop a struct from being an aggregate, so braced initialization keeps working exactly as it did before you added them. That is why every example in this lesson could mix the two freely.

Key Terminology

Default member initializer - an initializer attached to a non-static member inside the type definition, used whenever the object's own initializer does not supply a value for that member.

Non-static member initialization - the practice of writing default member initializers on non-static members.

Default initialization - creating an object with no braces (BakeSetting setting;). Members with defaults use them; members without are left uninitialized.

Value initialization - creating an object with empty braces (BakeSetting setting {};). Members with defaults use them; members without are zeroed.

Indeterminate value - the unspecified contents of an uninitialized member. Reading one is undefined behaviour.

Looking Forward

Default member initializers are the simplest of several ways a type can take charge of its own starting state. When we move from aggregates to classes, constructors take over the same job with more power: they can validate arguments, derive members from each other in any order, and offer several ways to build the same type. Default member initializers do not disappear at that point. They remain the concise way to state a member's usual starting value, and a constructor overrides one only when it has a reason to.

Summary

What a default member initializer is - an initializer written on a non-static member inside the type definition, such as int minutes {}; or int celsius { 180 };. It supplies that member's value whenever the object's own initializer does not.

The four outcomes - a supplied value always wins; a missing value falls back to the member's default member initializer; with no default, a missing value means value initialization inside braces and no initialization at all without braces.

Only one outcome is dangerous - a member with no default member initializer in an object defined without braces is left holding an indeterminate value, and reading it is undefined behaviour.

Give every member a default - an explicit value or an empty pair of braces on every member removes that outcome from your program regardless of how any call site defines its objects.

Prefer empty braces at the call site - BakeSetting setting {}; keeps working correctly if someone later adds a member without a default, and matches how you already initialize everything else.

Order and form matter - members initialize in declaration order, so a default may depend on members declared above it but never below. Write defaults with braces or =, never with parentheses, which the compiler parses as a function declaration.

Default member initializers move the definition of "sensible starting state" out of every call site and into the one place that should own it: the type itself.