c++c++17language-lawyerctor-initializervalue-initialization

C++17 Are non-static POD class members initialized with curly braces {} set to 0?


Got code review comment that my non-static class POD members be set to an invalid value. I agree that the initial value should be invalid. For my cases, 0 is invalid. Here's an example:

class A
{
  int a_{};  // Is a_ initialized to 0?
  double b_{}; // Is b_ initialized to 0.0?
  // etc.
};

I added the {} sometimes to handle compiler warnings that these class members should be initialized. I thought they would be initialized to zero. (Empirical observation indicates they are 0, but I am looking for authoritative sources one way or the other.)

I looked at these two links but could not say for certain that {} results in 0.
Meaning of default initialization changed in C++11?
Default and value initialization of POD types in C++

A clear simple source would be useful. I tried finding something here without luck.
https://eel.is/c++draft/


Solution

  • (I noticed the tags too late. My references are to the final draft for C++23. For C++17 the rules are effectively the same and they have been the same since default member initializers have been introduced in C++11, but there will have been some stuff moved around in the standard texts relative to the references below.)


    Assuming the default member initializer {} is actually used, i.e. the used constructor doesn't specify any other initializer for the given member, or, if this is an aggregate class, aggregate initialization doesn't specify any other initializer for the member, then the initialization of these members will be exactly the same as if their declarations appeared in some other context, e.g. as block scope variables. (See especially [class.base.init]/9.1.)

    So consider the {} initializer for types int/double. Initialization behavior is specified in long chains of decision rules starting in [dcl.init.general]/16. The first rule that applies is (16.1), because {} is a braced-init-list and all initialization with braces leads to list-initialization according to [dcl.init.list]/3.

    The first item to apply in this new decision chain is (3.11), which states that the object is value-initialized (because the initializer list is empty and the type isn't a class type or reference type or any other of the special cases before it in the chain).

    Value-initialization is defined in [dcl.init.general]/9 and in this decision chain the last item (9.3) is the first to apply, because neither int nor double are class or array types, and it states that the object is zero-initialized.

    Zero-initialization is defined in [dcl.init.general]/6 via another decision chain and there (6.1) already applies, because int and double are scalar types. It states that the object is initialized as if by converting an integer literal 0 to its type.

    At this point I hope it is clear that the int member's value will be 0 and double member's value will be 0.0 without going through the implicit conversion rules in [conv] as well.


    More generally: {} or, almost equivalently, = {} should in practice always be correct to initialize any object to its canonical "empty" state.

    For scalar types it is 0 converted to its type as shown above, for aggregate types it is the equivalent applied to each aggregate element recursively, for non-aggregate class types it is either a call to the default constructor if it is user-provided or full (recursive) zero-initialization followed by default initialization if the default constructor exists, but is not user-provided, or potentially a call to a std::initializer_list constructor with empty initializer list.

    So, except for class types with a constructor that is intentionally written to leave the object in an invalid or non-empty state, the result will be a fully initialized object to an "empty" state and if an object has a notion of an empty state that is independent of any inputs, then it should also be possible to initialize it with {}, assuming no evil class design.

    The only thing {} might not guarantee is zero-initialization of padding (i.e. as memset would permit for trivially-copyable types). Some forms of initialization do also guarantee that, but it isn't clear what that is even worth, because the standard doesn't specify that padding has stable values at all anyway (in contrast to C).