Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Preferred way of class member initialization?

class A { public: int x[100]; };

Declaring A a will not initialize the object (to be seen by garbage values in the field x). The following will trigger initialization: A a{} or auto a = A() or auto a = A{}.

Should any particular one of the three be preferred?

Next, let us make it a member of another class:

class B { public: A a; };

The default constructor of B appears to take care of initialization of a. However, if using a custom constructor, I have to take care of it. The following two options work:

class B { public: A a;  B() : a() { } };

or:

class B { public: A a{};  B() { } };

Should any particular one of the two be preferred?

like image 800
Lasse Kliemann Avatar asked Oct 19 '22 12:10

Lasse Kliemann


1 Answers

Initialization

class A { public: int x[100]; };

Declaring A a will not initialize the object (to be seen by garbage values in the field x).

Correct A a is defined without an initializer and does not fulfill any of the requirements for default initialization.


1) The following will trigger initialization:

A a{};

Yes;

  • a{} performs list initialization which
  • becomes value initialization if {} is empty, or could be aggregate initialization if A is an aggregate.
  • Works even if the default constructor is deleted. e.g. A() = delete; (If 'A' is still considered an aggregate)
  • Will warn of narrowing conversion.

2) The following will trigger initialization:

auto a = A();

Yes;

  • This is copy initialization where a prvalue temporary is constructed with direct initialization () which
    • uses value initialization if the () is empty.
    • No hope of aggregate initialization.
  • The prvalue temporary is then used to direct-initialize the object.
  • Copy elision may be, and normally is employed, to optimize out the copy and construct A in place.
    • Side effects of skipping copy/move constructors are allowed.
  • Move constructor may not be deleted. e.g A(A&&) = delete;
  • If copy constructor is deleted then move constructor must be present. e.g. A(const A&) = delete; A(A&&) = default;
  • Will not warn of narrowing conversion.

3) The following will trigger initialization:

auto a = A{}

Yes;

  • This is copy initialization where a prvalue temporary is constructed with list initialization {} which
    • uses value initialization if {} is empty, or could be aggregate initialization if A is an aggregate.
    • The prvalue temporary is then used to direct-initialize the object.
  • Copy elision may be, and normally is employed, to optimize out the copy and construct A in place.
    • Side effects of skipping copy/move constructors are allowed.
  • Move constructor may not be deleted. e.g A(A&&) = delete;
  • If copy constructor is deleted then move constructor must be present. e.g. A(const A&) = delete; A(A&&) = default;
  • Will warn of narrowing conversion.
  • Works even if the default constructor is deleted. e.g. A() = delete; (If 'A' is still considered an aggregate)

Should any particular one of the three be preferred?

Clearly you should prefer A a{}.


Member Initialization

Next, let us make it a member of another class:

class B { public: A a; };

The default constructor of B appears to take care of initialization of a.

No this is not correct.

  • the implicitly-defined default constructor of 'B' will call the default constructor of A, but will not initialize the members. No direct or list initialization will be triggered. Statement B b; for this example will call the default constructor, but leaves indeterminate values of A's array.

1) However, if using a custom constructor, I have to take care of it. The following two options work:

class B { public: A a;  B() : a() { } };

This will work;

  • : a() is a constructor initializer and a() is a member initializer as part of the member initializer list.
  • Uses direct initialization () or, if () is empty, value initialization.
  • No hope of using aggregate initialization.
  • Will not warn of narrowing conversion.

2) or:

class B { public: A a{};  B() { } };

This will work;

  • a now has a non-static data member initializer, which may require a constructor to initialize it if you are using aggregate initialization and the compiler is not fully C++14 compliant.
  • The member initializer uses list initialization {} which
  • may become either value initialization if {} is empty or aggregate initialization if A is an aggregate.
  • If a is the only member then the default constructor does not have to be defined and the default constructor will be implicitly defined.

Clearly you should prefer the second option.


Personally, I prefer using braces everywhere, with some exceptions for auto and cases where a constructor could mistake it for std::initializer_list:

class B { public: A a{}; };

A std::vector constructor will behave differently for std::vector<int> v1(5,10) and std::vector<int> v1{5,10}. with (5,10) you get 5 elements with the value 10 in each one, but with {5,10} you get two elements containing 5 and 10 respectively because std::initializer_list is strongly preferred if you use braces. This is explained very nicely in item 7 of Effective Modern C++ by Scott Meyers.

Specifically for member initializer lists, two formats may be considered:

  • Direct initialization a() which becomes value initialization if the () is empty.
  • List initialization a{} which also becomes value initialization if {} is empty.

In member initializer lists, fortunately, there is no risk of the most vexing parse. Outside of the initializer list, as a statement on its own, A a() would have declared a function vs. A a{} which would have been clear. Also, list initialization has the benefit of preventing narrowing conversions.

So, in summary the answer to this question is that it depends on what you want to be sure of and that will determine the form you select. For empty initializers the rules are more forgiving.

like image 186
wally Avatar answered Oct 21 '22 22:10

wally