Reputation: 919
I know it's a subject that has been discussed quite extensively here on stackoverflow but I've a hard time finding thorough answers removing all the confusion I've concerning list initialization and initializer_lists in C++, so I will just give it a try and ask my own questions.
Please, consider the following snippet of code :
class C
{
public :
C(int a, int b, int c) : _a (a), _b(b), _c(c) {}; //initialization_list with ()
//C(int a, int b, int c) : _a{ a }, _b{ b }, _c{ c } {}; //initialization list with {}
private :
int _a, _b, _c;
};
int main()
{
C a(5.3,3.3,4.3); // no list
C b{5.3,3.3,4.3}; // list {}
C c({5.3,3.3,4.3}); // list {}
}
I don't understand why those two initialization lists behave similarly? I was expecting, when trying to create the object a of type C using the initialization list of type _a{a}, _b{b}, _c{c}
to get a compiler error about narrowing. However, no errors are generated and _a, _b and _c
just store the integer values.
It's only when creating the objects b or c using a list "{}" that the compiler generates a narrowing error message. Why is that? Is there any differences between writing an initialization list using {} or () that I'm unaware of or are the behaviours identical?
Come my next question:
class C
{
public :
//private :
int _a, _b, _c;
};
int main()
{
C a(5,3,4); //obviously doesn't work as no such constructor
C b{5,3,4}; //work only if _a, _b and _c are not private nor protected!
}
How comes that the second statement (with braces) only works if the variables are public? What is the mechanism involved?
So I would like to better understand, beside the "narrowing safety" provided by creating an object with a list {}, what other "functionalities" does this list mechanism provide ? Because in the second call, it's still the default constructor that is called (hence, not a default constructor taking an initializer_list as argument), right ?
Lastly, imagine in my class C
I've another constructor taking an initializer list as parameter.
class C
{
public :
C() = default; //default constructor
C(int a, int b, int c) : _a (a), _b(b), _c(c) {};
C(std::initializer_list<int> a) { //do some stuffs with the list};
private :
int _a, _b, _c;
};
It's pretty obvious that, if trying to create an object taking any numbers of integers but 3 (or 0 actually), the constructor taking the initializer_list will be invoked. If creating an object like that however :
C c();
or
C c{};
the default constructor will be called. But if creating an object with exactly 3 integers :
C c(5,2,3);
or
C c{5,2,3};
the initializer_list constructor will be called. The rule goes like that :
- If either a default constructor or an initializer-list constructor could be invoked, prefer the default constructor
- If both an initializer-list contructor and an "ordinary constructor" could be invoked, prefer the initializer-list constructor
Therefore (and correct me if I'm wrong), if I create my object like that :
C c{5,3,4};
The iniatializer-list constructor will be called. However, if I create my object like that :
C c(5,3,4);
The second constructor (taking 3 ints as arguments) will be called. My question is : how can I create an object with this second constructor instead of the iniatializer-list one if I want to provide narrowing safety as well ? (because if I do as in the first example of this question, the initializer-list constructor will be called instead !).
Don't hesitate to examplify your replies and to discuss on list-related concepts I haven't talk in this question. I would like to get a very good grasp on those. Thanks.
Upvotes: 2
Views: 1565
Reputation: 2968
So anytime curly braces are used you are using aggregate initialization, a method of initialization for structs or classes that initializes in order., or via a designator. For example,
#include <iostream>
struct Foo
{
int a;
char b;
};
class Doo
{
public:
double h;
char ch;
};
int main() {
Foo first = {3, 't'};
std::cout << first.b << "\n";
//t
Doo second = {'3', 50};
std::cout << second.ch << "\n";
//2 interpreted as char
}
Here, when we use the {}
to initialize a class or struct, they are always interpreted as being in the order listed in the class. That's why '2' was printed since 50 in ASCII corresponds to the character '2'.
Constructor Initialization
So you can also use the same logic with constructor initialization lists,
#include <iostream>
struct Pair
{
int first;
long second;
};
class Couple
{
public:
Pair p;
int g;
public:
Couple(): p{3, 700}, g{3}
{}
};
int main() {
Couple test;
std::cout << test.p.first << "\n";
//3
}
Here, the {3, 700}
next to p
, would be the same as Pair p = {3, 700};
used else where in code. Your basically using an in order aggregate initialization. Now, what happens if we change the curly braces for the Pair field to parenthesis?
We get this error
main.cpp: In constructor 'Couple::Couple()':
main.cpp:15:26: error: no matching function for call to 'Pair::Pair(int, int)'
Couple(): p(3, 700), g{3}
That's because we don't have a constructor for Pair that accepts two numbers. So the key difference between the aggregate initialization and the parenthesis is you need to have constructors implemented for any specific set of arguments you make with parenthesis, yet with curly braces you can just use the default one the compiler hands you.
The std::initializer_list is a not-commonly used form of container for multiple arguments in initialization lists with {}
.
Upvotes: 4