Reputation: 2481
I've searched the answer(s) to this question online but I have yet to find a satisfactory answer. I was wondering what are all the rules for initialization of objects of struct and class types, specifically when it comes to constructors vs braced initializer lists. Are the rules different for structs vs classes too?
Let us suppose we have a class or struct called Rectangle
.
#include <iostream>
using namespace std;
class Rectangle {
public:
Rectangle() : x(5.0), y(6.0), width(7.0), height(8.0) {}
void printMe()
{
cout << "The rectangle is located at (" << x << ',' << y << ") and is " << width << " x " << height << endl;
}
double x;
double y;
double width;
double height;
};
int main()
{
Rectangle r = {0.0, 0.0, 3.0, 4.0};
r.printMe();
Rectangle s; // uninitialized!
s.printMe();
}
I attempt to initialize Rectangle r
the way you would usually do it in C, using a plain old braced initializer list. However, g++
gives the following error:
constructor_vs_initializer_list.cpp: In function ‘int main()’:
constructor_vs_initializer_list.cpp:21:38: error: could not convert ‘{0.0, 0.0, 3.0e+0, 4.0e+0}’ from ‘<brace-enclosed initializer list>’ to ‘Rectangle’
Rectangle r = {0.0, 0.0, 3.0, 4.0};
^
Hmmm.... That is not very useful error message it seems at first glance. However I think that it has something to do with the constructor, because if I remove it, the code compiles and runs! It would be a paradox I think, both the braced initializer list and the constructor are competing to initialize the data members it seems.
However, when I made the data members private
, after removing the constructor that is, the same error message was displayed again!
I wonder what are the rules of precedence in initialization of data members. How does the braced initializer list compare to a constructor you defined yourself? How does it compare to the C++11 features: = default
constructor and in-class member initializers? I assume that these different ways of initializing the object's data members would conflict with each other somehow.
Rectangle() = default;
...
double x = 1.0;
I'm not writing that mixing them up is necessarily good code, just it is code, and code that should be understood well in my opinion. Thank you.
Upvotes: 3
Views: 3489
Reputation: 1371
Here's an example demonstrating the differences. Initialization in C++ is pretty complex. See: https://blog.tartanllama.xyz/initialization-is-bonkers/.
It's usually best to use default member initializers or initialization lists. You're doing the right thing in your constructor. Just call the constructor with direct-list-initialization or direct initialization to avoid confusing people. Generally, you would only use copy-list-initialization to initialize aggregates without user-provided constructors.
#include <iostream>
struct A {
int i;
};
struct B {
B() = default;
int i;
};
struct C {
C();
int i;
};
C::C() = default;
struct D {
D(){};
int i;
};
struct E : public D {
};
struct F {
F(int i = 5) {}
int i;
};
struct G {
G() = delete;
int i;
};
int main() {
// g++ (v 8.2.1) provides good warnings about uninitialized values.
// clang++ (v 7.0.1) does not.
// Technically, they are initialized to 'indeterminate values', but it is
// easier to refer to the member variables as uninitialized.
{
// All of the following are 'default initialized', meaning they are not even
// zero-initialized. Members are UNINITIALIZED (Technically, they are
// initialized to 'indeterminate' values.
// Either nothing is done, or the default constructor is called (in
// which nothing is done).
A a;
B b;
C c;
D d;
E e;
F f;
std::cout << "a: " << a.i << std::endl;
std::cout << "b: " << b.i << std::endl;
std::cout << "c: " << c.i << std::endl;
std::cout << "d: " << d.i << std::endl;
std::cout << "e: " << e.i << std::endl;
std::cout << "f: " << f.i << std::endl;
std::cout << std::endl;
} {
// This is more complex, as these are all 'list initialized'.
// Thank you, infinite wisdom of the C++ committee.
A a{};
// Direct list initialization -> aggregate initialization
// - A has no user-provided constructor and
// thus is an aggregate, and agg. init. takes place.
// This 'value initializes' all *MEMBERS* (unless a default member
// initializer exists, which it does not here).
// Value initialization of non-class types results in
// zero-initialization. (member `i` is zero-initialized)
A a2 = {};
// same thing, but via copy list initialization
A a3{{}};
// recursive, initializes `i` with {}, which zero initializes `i`.
A a4{0};
// recursive, initializes `i` 0;
// Could also do `A a4 = {0}`
A a5{a};
// direct intialization of `a5` with `a`.
// Implicit copy constructor chosen by overload resolution.
A a6{A{}};
// post C++17, direct initializes a6 with a prvalue of type A, that is
// aggregate initialized as above. NOT copy/move initialized, but
// instead initialized via the "initializer expression itself".
// I assume this means the value of a6 is directly set via as if it were
// being aggregate initialized.
B b{};
// Same as A. `B() = default;` does NOT specify a user-provided
// constructor
C c{};
// Because the first declaration of `C()` is not `C() = default;`,
// this DOES have a user-provided constructor, and 'value initializaton'
// is performed.
// NOTE: this value intializes `C`, not the *MEMBERS* of `C`.
// Because `C` is a normal class type, value initialization just calls
// the default constructor, which does nothing, and leaves all members
// uninitialized.
D d{};
// D is a class type that is list/direct initialization -> value
// inititalizaton -> default initialization -> call constructor ->
// members are left unitialized.
E e{};
// List initialization -> value initialization -> default initialization
// -> calls implicitly defined default constructor -> Calls default
// constructor of bases -> leaves E::D.i uninitialized
F f{};
// List/direct initialization -> value initialization -> calls default
// constructor with default arguments -> leaves F.i uninitialized
// G g{};
// Fails to compile.
// list initialization -> value initialization -> default initialization
// -> deleted default constructor selected by overload resolution ->
// fails to compile
std::cout << "a: " << a.i << std::endl;
std::cout << "a2: " << a2.i << std::endl;
std::cout << "a3: " << a3.i << std::endl;
std::cout << "a4: " << a4.i << std::endl;
std::cout << "a5: " << a5.i << std::endl;
std::cout << "a6: " << a6.i << std::endl;
std::cout << "b: " << b.i << std::endl;
std::cout << "c: " << c.i << std::endl;
std::cout << "d: " << d.i << std::endl;
std::cout << "e: " << e.i << std::endl;
std::cout << "f: " << f.i << std::endl;
}
}
Upvotes: 5
Reputation: 26800
The CPP standard draft n4713 states this about aggregate initialization:
11.6.1 Aggregates [dcl.init.aggr]
1 An aggregate is an array or a class with
(1.1) — no user-provided, explicit, or inherited constructors,
(1.2) — no private or protected non-static data members
In your case you have a user-provided constructor in the first case and private data members in the second case which violate (1.1) and (1.2) bullet points of the above respectively.
Upvotes: 2