The following code successfully compiles with most modern C++11 compatible compilers (GCC >= 5.x, Clang, ICC, MSVC).
#include <string>
struct A
{
explicit A(const char *) {}
A(std::string) {}
};
struct B
{
B(A) {}
B(B &) = delete;
};
int main( void )
{
B b1({{{"test"}}});
}
But why does it compile in the first place, and how are the listed compilers interpreting that code?
Why is MSVC able to compile this without B(B &) = delete;
, but the other 3 compilers all need it?
And why does it fail in all compilers except MSVC when I delete a different signature of the copy constructor, e.g. B(const B &) = delete;
?
Are the compilers even all choosing the same constructors?
Why does Clang emit the following warning?
17 : <source>:17:16: warning: braces around scalar initializer [-Wbraced-scalar-init]
B b1({{{"test"}}});
Instead of explaining the behavior of compilers, I'll try to explain what the standard says.
To direct-initialize b1
from {{{"test"}}}
, overload resolution applies to choose the best constructor of B
. Because there is no implicit conversion from {{{"test"}}}
to B&
(list initializer is not a lvalue), the constructor B(B&)
is not viable. We then focus on the constructor B(A)
, and check whether it is viable.
To determine the implicit conversion sequence from {{{"test"}}}
to A
(I will use the notation {{{"test"}}}
-> A
for simplicity), overload resolution applies to choose the best constructor of A
, so we need to compare {{"test"}}
-> const char*
and {{"test"}}
-> std::string
(note the outermost layer of braces is elided) according to [over.match.list]/1:
When objects of non-aggregate class type T are list-initialized such that [dcl.init.list] specifies that overload resolution is performed according to the rules in this subclause, overload resolution selects the constructor in two phases:
Initially, the candidate functions are the initializer-list constructors ([dcl.init.list]) of the class T...
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.
... In copy-list-initialization, if an explicit constructor is chosen, the initialization is ill-formed.
Note all constructors are considered here regardless of the specifier explicit
.
{{"test"}}
-> const char*
does not exist according to [over.ics.list]/10 and [over.ics.list]/11:
Otherwise, if the parameter type is not a class:
if the initializer list has one element that is not itself an initializer list...
if the initializer list has no elements...
In all cases other than those enumerated above, no conversion is possible.
To determine {{"test"}}
-> std::string
, the same process is taken, and overload resolution chooses the constructor of std::string
that takes a parameter of type const char*
.
As a result, {{{"test"}}}
-> A
is done by choosing the constructor A(std::string)
.
explicit
is removed?The process does not change. GCC will choose the constructor A(const char*)
while Clang will choose the constructor A(std::string)
. I think it is a bug for GCC.
b1
?Note {{"test"}}
-> const char*
does not exist but {"test"}
-> const char*
exists. So if there are only two layers of braces in the initializer of b1
, the constructor A(const char*)
is chosen because {"test"}
-> const char*
is better than {"test"}
-> std::string
. As a result, an explicit constructor is chosen in copy-list-initialization (initialization of the parameter A
in the constructor B(A)
from {"test"}
), then the program is ill-formed.
B(const B&)
is declared?Note this also happens if the declaration of B(B&)
is removed. This time we need to compare {{{"test"}}}
-> A
and {{{"test"}}}
-> const B&
, or {{{"test"}}}
-> const B
equivalently.
To determine {{{"test"}}}
-> const B
, the process described above is taken. We need to compare {{"test"}}
-> A
and {{"test"}}
-> const B&
. Note {{"test"}}
-> const B&
does not exist according to [over.best.ics]/4:
However, if the target is
— the first parameter of a constructor or
— the implicit object parameter of a user-defined conversion function
and the constructor or user-defined conversion function is a candidate by
— [over.match.ctor], when the argument is the temporary in the second step of a class copy-initialization,
— [over.match.copy], [over.match.conv], or [over.match.ref] (in all cases), or
— the second phase of [over.match.list] when the initializer list has exactly one element that is itself an initializer list, and the target is the first parameter of a constructor of class X, and the conversion is to X or reference to cv X,
user-defined conversion sequences are not considered.
To determine {{"test"}}
-> A
, the process described above is taken again. This is almost the same as the case we talked in the previous subsection. As a result, the constructor A(const char*)
is chosen. Note the constructor is chosen here to determine {{{"test"}}}
-> const B
, and does not apply actually. This is permitted though the constructor is explicit.
As a result, {{{"test"}}}
-> const B
is done by choosing the constructor B(A)
, then the constructor A(const char*)
. Now both {{{"test"}}}
-> A
and {{{"test"}}}
-> const B
are user-defined conversion sequences and neither is better than the other, so the initialization of b1
is ambiguous.
According to [over.best.ics]/4, which is block-quoted in the previous subsection, the user defined conversion {{{"test"}}}
-> const B&
is not considered. So the result is the same as the primary example even if the constructor B(const B&)
is declared.
B b1({{{"test"}}});
is like B b1(A{std::string{const char*[1]{"test"}}});
16.3.3.1.5 List-initialization sequence [over.ics.list]
4 Otherwise, if the parameter type is a character array 133 and the initializer list has a single element that is an appropriately-typed string literal (11.6.2), the implicit conversion sequence is the identity conversion.
And the compiler tries all possible implicit conversions. For example if we have class C with the following constructors:
#include <string>
struct C
{
template<typename T, size_t N> C(const T* (&&) [N]) {}
template<typename T, size_t N> C(const T (&&) [N]) {}
template<typename T=char> C(const T* (&&)) {}
template<typename T=char> C(std::initializer_list<char>&&) {}
};
struct A
{
explicit A(const char *) {}
A(C ) {}
};
struct B
{
B(A) {}
B(B &) = delete;
};
int main( void )
{
const char* p{"test"};
const char p2[5]{"test"};
B b1({{{"test"}}});
}
The clang 5.0.0 compiler could not decide which to use and fails with:
29 : <source>:29:11: error: call to constructor of 'C' is ambiguous
B b1({{{"test"}}});
^~~~~~~~~~
5 : <source>:5:40: note: candidate constructor [with T = char, N = 1]
template<typename T, size_t N> C(const T* (&&) [N]) {}
^
6 : <source>:6:40: note: candidate constructor [with T = const char *, N = 1]
template<typename T, size_t N> C(const T (&&) [N]) {}
^
7 : <source>:7:39: note: candidate constructor [with T = char]
template<typename T=char> C(const T* (&&)) {}
^
15 : <source>:15:9: note: passing argument to parameter here
A(C ) {}
^
But if we leave only one of the non-initializer-list constructors the code compiles fine.
GCC 7.2 just picks the C(const T* (&&)) {}
and compiles. If it's not available it takes C(const T* (&&) [N])
.
MSVC just fails with:
29 : <source>(29): error C2664: 'B::B(B &)': cannot convert argument 1 from 'initializer list' to 'A'
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With