Different ways of calling an initializer-list-constructor - c++

Consider this example for initializer-list-constructor usage:
std::vector<std::string> v = { "xyzzy", "plugh", "abracadabra" };
std::vector<std::string> v({ "xyzzy", "plugh", "abracadabra" });
std::vector<std::string> v{ "xyzzy", "plugh", "abracadabra" };
Are there any differences (even slightly) between them?
In a large project, where you have to define a standard, which style would you choose?
I would prefer the first style, the third can be easly confused with a call to a constructor with args. Also the first style looks familiar to other programming languages.

In case of a vector of strings, there's no difference between the three forms. There can, however, be a difference between the first and the other two if the constructor taking the initializer_list is explicit. In that case, the first, which is copy-list-initialization, is not allowed, while the other two, which are direct-list-initialization, are allowed.
Because of that reason, my preference would be the third form. I'd avoid the second because the parentheses are redundant.
Further differences arise, as Yakk points out in the comments, when the type being constructed does not have a constructor taking an initializer_list.
Say for instance, the type being constructed has a constructor that takes 3 arguments, all of type char const *, instead of the initializer_list constructor. In that case, forms 1 & 3 are valid, but 2 is ill-formed, because the braced-init-list cannot match the 3 argument constructor when enclosed in parentheses.
If the type does have an initializer list constructor, but the elements of the braced-init-list are not implicitly convertible to initializer_list<T>, then other constructors will be considered. Assuming another constructor that is a match exists, form 2 results in an intermediate copy being constructed, while the other two don't. This can be demonstrated by the following example, compiled with -fno-elide-constructors.
struct foo
{
foo(int, int) { std::cout << __PRETTY_FUNCTION__ << std::endl; }
foo(foo const&) { std::cout << __PRETTY_FUNCTION__ << std::endl; }
foo(std::initializer_list<std::string>) { std::cout << __PRETTY_FUNCTION__ << std::endl; }
};
int main()
{
foo f1 = {1,2};
std::cout << "----\n";
foo f2({1,2});
std::cout << "----\n";
foo f3{1,2};
}
Output:
foo::foo(int, int)
----
foo::foo(int, int)
foo::foo(const foo&)
----
foo::foo(int, int)
The following case is not part of the question, but still good to be aware of. Using nested braces can result in unintuitive behavior in certain cases. Consider
std::vector<std::string> v1{{ "xyzzy", "plugh", "abracadabra" }};
std::vector<std::string> v2{{ "xyzzy", "plugh"}};
v1 works as expected and will be a vector containing 3 strings, while v2 results in undefined behavior. Refer to this answer for a detailed explanation.

Related

Understanding how compiler uses plain {}-list inside initialization

Consider the following toy code:
class Y
{
public:
Y(int, int) { cout << "Y ctor\n"; }
};
class X
{
public:
//X(initializer_list<int>) { cout << "init\n"; } // 1
X(Y&&) { cout << "Y&&\n"; } // 2
X(int, int) { cout << "param\n"; } // 3
X(const X&) { cout << "X&\n"; } // 4
X(const Y&) { cout << "Y&\n"; } // 5
X(Y) { cout << "Y\n"; } // 6
X(X&&) { cout << "X&&\n"; } // 7
};
void f(const X&) { }
void f(const Y&) { }
void f(Y) { }
int main()
{
X x({1,2});
//f({ 1,2 }); // 8
}
I'm trying to understand how the compiler uses {1,2} in X x({1,2}). Followings are my understandings:
Uncomment line 1
In such case, I think X x({1,2}) is an explicit call for X(initializer_list<int>). That is, when compiler sees {}-list, I think it will first see it as std::initializer_list and a constructor that takes an argument of initializer_list<T> will be the better match than all the others.
Comment out line 6, 7
In such case, I think {1,2} is used to construct an object of Y which is bound with an rvalue reference (i.e. compiler chooses to call X(Y&&)). My guess here is that the compiler treats {1,2} as some kind of rvalue when there is no constructor that takes an argument of initializer_list<T>, and thus a constructor that takes an rvalue reference is preferred (in this case, X(Y&&) is the only constructor of this kind).
Update: I found that the behavior I described in this bullet point is actually a bug of MSVC. gcc and clang will report ambiguity, not calling X(Y&&)
Comment out line 7
Ambiguity arises. Compiler reports that both X(Y&&) and X(Y) match. I think this is because although X(Y&&) is a better match than X(const Y&) and X(const X&), it is indistinguishable to X(Y). (I think the logic here is quite twisted)
Possible reference.
Comment out line 2,7
The code compiles and prints param. I think this time X(const X&) is chosen but I'm not sure why. My guess for this is that, when all the viable matches (X(const X&), X(const Y&) and X(Y)) are indistinguishable, the compiler chooses X(const X&) because X x({1,2}); is constructing an X.
Update: this can also be a MSVC bug. gcc and clang will report ambiguity, not calling X(const X&)
Comment out line 2,7 and uncomment line 8
I think this is the case where all the viable matches are indistinguishable and f is not a constructor of X, hence f(const X&) is not treated specially, and ambiguity arises.
May I ask if my understandings are correct? (I highly doubt they are accurate)
I find that it is quite tedious to read out what constructor is invoked by things like X x({1,2});, so I guess in real code we should try to avoid such shorthand and write code in a more explicit way...
On the other hand, if we use the same settings for class X and Y as above, define and call a function g as follows:
X g()
{
return { 1,2 };
}
int main()
{
g();
}
The results always seem to be equivalent as initializing the returned temporary by X{1,2} (i.e. call X(initializer_list<int>) when it exists, and in all the other cases call X(int, int)). May I ask if this is true?
X x({1,2}) can’t be an “explicit call for” a std::initializer_list: a braced-init-list has no type. However, there is a rule saying that one is a better match for a specialization of that template than for any other class type.
X(Y&&) is used because X(const X&) isn’t viable. It would be considered a second user-defined conversion even though {…}→Y→X isn’t (because of the two layers of initialization).
Yes, Q and Q&& are often ambiguous like that. Consider that C++17 prvalues don’t even move to initialize either.
Presumably you meant you commented out lines 2 and 6. In that case, the constructors from (references to) X are disqualified as above, and X(const Y&) is a worse match because the const must be added to the non-const list-initialized prvalue. (All user-defined conversions are otherwise indistinguishable.)
f(Y) is better than the other two regardless of X’s constructors for the same const reason.
As for g, yes copy-list-initialization is much the same as direct-list-initialization in the absence of explicit constructors. Since it is list-initialization, rather than direct initialization from an “argument” that is a braced-init-list, it does prefer an initializer-list constructor over all others.

Why are many curly brackets treated differently by C++ compilers?

In the following C++20 program I put by mistake one extra pair of curved braces {} in B{{{{A{}}}}}:
#include <iostream>
struct A
{
A() { std::cout << "A() "; }
A( A&& ) = delete;
~A() { std::cout << "~A() "; }
};
struct B { std::initializer_list<A> l; };
int main()
{
[[maybe_unused]] auto x = B{{{{A{}}}}};
std::cout << ". ";
}
Clang rejected it, however with a strange error:
error: call to deleted constructor of 'const A'
But to my surprise GCC accepted it( https://gcc.godbolt.org/z/aPWe13xfc ).
Could you please explain why GCC accepts it (how does it treat extra curved braces)?
B{…}, since the single element of the initializer list is not designated and is not of type B (as it has no type at all), is aggregate initialization ([dcl.init.list]/3.4). B::l is thus copy-initialized from {{{A{}}}}; it's a specialization of std::initializer_list, so /3.6 and /5 apply. An "array of 1 const A" is created, and {{A{}}} is the initializer for its single element.
We can thus reduce the code to
const A a = {{A{}}};
with no mention of B at all, and indeed Clang and GCC produce the same disagreement for this line. Clang appears to be correct to reject it: that initialization is sent to constructors by /3.7, and obviously there is no constructor that could be viable (thus the error about the deleted move constructor).
Oddly, removing the extra pair of braces here (or in the original) causes both compilers to accept:
const A a = {A{}};
despite the fact that A is not an aggregate and so /3.7 still applies. Presumably both compilers are overenthusastically performing "guaranteed copy elision" (albeit to different degrees), identifying the prvalue A{} with an object eventually to be initialized by it; that, however, takes place only per [dcl.init.general]/16.6.1, which never comes into play in this analysis.

Direct list initialization, which constructor will be used? [duplicate]

Consider the code
#include <iostream>
class Foo
{
int val_;
public:
Foo(std::initializer_list<Foo> il)
{
std::cout << "initializer_list ctor" << std::endl;
}
/* explicit */ Foo(int val): val_(val)
{
std::cout << "ctor" << std::endl;
};
};
int main(int argc, char const *argv[])
{
// why is the initializer_list ctor invoked?
Foo foo {10};
}
The output is
ctor
initializer_list ctor
As far as I understand, the value 10 is implicitly converted to a Foo (first ctor output), then the initializer constructor kicks in (second initializer_list ctor output). My question is why is this happening? Isn't the standard constructor Foo(int) a better match? I.e., I would have expected the output of this snippet to be just ctor.
PS: If I mark the constructor Foo(int) as explicit, then Foo(int) is the only constructor invoked, as the integer 10 cannot now be implicitly converted to a Foo.
§13.3.1.7 [over.match.list]/p1:
When objects of non-aggregate class type T are list-initialized
(8.5.4), overload resolution selects the constructor in two phases:
Initially, the candidate functions are the initializer-list constructors (8.5.4) of the class T and the argument list consists of
the initializer list as a single argument.
If no viable initializer-list constructor is found, overload resolution is performed again, where the candidate functions are all
the constructors of the class T and the argument list consists of
the elements of the initializer list.
If the initializer list has no elements and T has a default
constructor, the first phase is omitted. In copy-list-initialization,
if an explicit constructor is chosen, the initialization is
ill-formed.
As long as there is a viable initializer-list constructor, it will trump all non-initializer-list constructors when list-initialization is used and the initializer list has at least one element.
The n2100 proposal for initializer lists goes into great detail about the decision to make sequence constructors (what they call constructors that take std::initializer_lists) to have priority over regular constructors. See Appendix B for a detailed discussion. It's succinctly summarized in the conclusion:
11.4 Conclusion
So, how do we decide between the remaining two alternatives (“ambiguity” and “sequence constructors take priority
over ordinary constructors)? Our proposal gives sequence constructors
priority because
Looking for ambiguities among all the constructors leads to too many “false positives”; that is, clashes between apparently unrelated
constructors. See examples below.
Disambiguation is itself error-prone (as well as verbose). See examples in §11.3.
Using exactly the same syntax for every number of elements of a homogeneous list is important – disambiguation should be done for
ordinary constructors (that do not have a regular pattern of
arguments). See examples in §11.3. The simplest example of a false
positive is the default constructor:
The simplest example of a false positive is the default constructor:
vector<int> v;
vector<int> v { }; // potentially ambiguous
void f(vector<int>&);
// ...
f({ }); // potentially ambiguous
It is possible to think of classes where initialization with no
members is semantically distinct from default initialization, but we
wouldn’t complicate the language to provide better support for those
cases than for the more common case where they are semantically the
same.
Giving priority to sequence constructors breaks argument checking into
more comprehensible chunks and gives better locality.
void f(const vector<double>&);
// ...
struct X { X(int); /* ... */ };
void f(X);
// ...
f(1); // call f(X); vector’s constructor is explicit
f({1}); // potentially ambiguous: X or vector?
f({1,2}); // potentially ambiguous: 1 or 2 elements of vector
Here, giving priority to sequence constructors eliminates the
interference from X. Picking X for f(1) is a variant of the problem
with explicit shown in §3.3.
The whole initializer list thing was meant to enable list initialisation like so:
std::vector<int> v { 0, 1, 2 };
Consider the case
std::vector<int> v { 123 };
That this initializes the vector with one element of value 123 rather than 123 elements of value zero is intended.
To access the other constructor, use the old syntax
Foo foo(10);

direct-initialization vs direct-list-initialization (C++)

DIRECT- VS COPY-INITIALIZATION
Through this question (Is it direct-initialization or copy-initialization?) I learned the differences between direct-initialization and copy-initialization:
direct-initialization copy-initialization
----------------------- ---------------------
obj s("value"); obj s = obj("value");
obj s = "value";
obj s{"value"}; obj s = {"value"};
obj s = obj{"value"};
I mention it here for the sake of completeness. My actual questions for this page are listed in the next paragraph >>
DIRECT-INITIALIZATION VS DIRECT-LIST-INITIALIZATION
The answers revealed that within the category of direct-initialization, one can make a difference between direct-initialization and direct-list-initialization.:
obj s("value"); // direct-initialization
obj s{"value"}; // direct-list-initialization
I know that list-initialization doesn't allow narrowing, such that an initialization like int x{3.5}; won't compile. But besides this, I got a couple of questions:
(1) Is there any difference in compiler output between
obj s("value"); and obj s{"value"};?
Let's consider a compiler without any optimizations. I would like to know any possible technical difference :-)
(2) Perhaps I should ask the exact same question for a multi-variable initialization, like:
obj s("val1", "val2"); and obj s{"val1", "val2"};
(3) I have noticed that the list-initialization can sometimes call a different constructor, like in:
vector<int> a{10,20}; //Curly braces -> fills the vector with the arguments
vector<int> b(10,20); //Parentesis -> uses arguments to parameterize some functionality
How is that possible?
DID WE COVER ALL POSSIBLE INITIALIZATIONS HERE?
From my limited knowledge on C++, I believe that all possible initializations of objects (either native-typed or user-defined-typed objects) have been covered in the examples above. Is that correct? Did I overlook something?
PS: I am learning C++ (I do know C, but not yet C++), so please don't be too hard on me ;-)
(1) Is there any difference in compiler output between
obj s("value"); and obj s{"value"};? Let's consider a compiler
without any optimizations. I would like to know any possible technical
difference :-)
(2) Perhaps I should ask the exact same question for a
multi-variable initialization, like: obj s("val1", "val2"); and
obj s{"val1", "val2"};
(3) I have noticed that the list-initialization can
sometimes call a different constructor, like in:
vector<int> a{10,20}; //Curly braces -> fills the vector with the arguments
vector<int> b(10,20); //Parentesis -> uses arguments to parameterize some functionality
How is that possible?
If there is an initializer-list constructor for the class type obj, it will always be preferred over other other constructors for brace-init-initializers (list initialization), in your case obj s{"value"};;
What this means is that if you have a constructor that takes std::initializer_list<T> as its first parameter and other parameters are defaulted, then it is preferred. Example
struct A{
A(std::initializer_list<std::string>); //Always be preferred for A a{"value"}
A(std::string);
};
std::vector<T> and other STL containers have such initializer-list constructors.
Otherwise, Overload resolution kicks in and it falls back to any available constructor as selected by the overload resolution process;
Otherwise, if the class has no user defined constructors and it's an aggregate type, it initializes the class members directly.
DID WE COVER ALL POSSIBLE INITIALIZATIONS HERE? From my
limited knowledge on C++, I believe that all possible initializations
of objects (either native-typed or user-defined-typed objects) have
been covered in the examples above. Is that correct? Did I overlook
something?
Nope. you didn't. Excluding Reference initialization, there are five ways objects may be initialized in C++.
Direct Initialization
List Initialization
Copy Initialization
Value Initialization
Aggregate Initialization (only for aggregate types)
You can find more information here
List-initialization guarantee left-to-right order of arguments evaluation. In this example we will create std::tuple from istream data and then output tuple example can be found here:
#include <iostream>
#include <sstream>
#include <tuple>
template<typename T, typename CharT>
T extract(std::basic_istream<CharT>& is) {
T val;
is >> val;
return val;
}
void print(const std::tuple<int, long, double>& t) {
using std::cout;
cout << std::get<0>(t) << " " << std::get<1>(t) << " " << std::get<2>(t) << std::endl;
}
int main()
{
std::stringstream ss1;
std::stringstream ss2;
ss1 << 1 << " " << 2 << " " << 3;
ss2 << 1 << " " << 2 << " " << 3;
auto compilerOrder = std::tuple<int, long, double>( extract<int>(ss1), extract<long>(ss1), extract<double>(ss1) );
auto leftToRightOrder = std::tuple<int, long, double>{ extract<int>(ss2), extract<long>(ss2), extract<double>(ss2) };
print(compilerOrder);
print(leftToRightOrder);
}
Output:
3 2 1
1 2 3
As you can see, the difference will be seen then we use multiple times same stream-like resource inside function brackets.
Also my question about that

DR 2137 is not clear to me

In DR 2137 we have the following text (emphasis is mine):
It is not clear in code like the following that selecting a copy/move
constructor is the correct choice when an initializer list contains a
single element of the type being initialized, as required by issue
1467:
#include <initializer_list>
#include <iostream>
struct Q {
Q() { std::cout << "default\n"; }
Q(Q const&) { std::cout << "copy\n"; }
Q(Q&&) { std::cout << "move\n"; }
Q(std::initializer_list<Q>) { std::cout << "initializer list\n"; }
};
int main() {
Q x = Q { Q() };
}
Here the intent is that Q objects can contain other Q objects, but
this is broken by the resolution of issue 1467.
I'd like to understand why the code above would be broken by the resolution of issue 1467.
DR 2147 has this statement: "the intent is that Q objects can contain other Q objects". Given that, it assumes that if the user constructs a Q from a braced-init-list containing other Qs, that the intent of the user is to call the initializer_list constructor.
Given that assumption, it is therefore "broken" to not call the initializer_list constructor, which 1467 would cause.
Whether you agree with this logic or not, that's the thinking behind 2147. It's also the thinking that permits [over.match.list] to prioritize initializer_list constructors over any other constructor type. So in that way, it is consistent.