Consider the following:
struct A { A(int, int) { } }; struct B { B(A ) { } // (1) explicit B(int, int ) { } // (2) }; int main() { B paren({1, 2}); // (3) B brace{1, 2}; // (4) }
The construction of brace
in (4)
clearly and unambiguously calls (2)
. On clang, the construction of paren
in (3)
unambiguously calls (1)
where as on gcc 5.2, it fails to compile with:
main.cpp: In function 'int main()': main.cpp:11:19: error: call of overloaded 'B(<brace-enclosed initializer list>)' is ambiguous B paren({1, 2}); ^ main.cpp:6:5: note: candidate: B::B(A) B(A ) { } ^ main.cpp:5:8: note: candidate: constexpr B::B(const B&) struct B { ^ main.cpp:5:8: note: candidate: constexpr B::B(B&&)
Which compiler is right? I suspect clang is correct here, as the ambiguity in gcc can only arise through a path that involves implicitly constructing B{1,2}
and passing that to the copy/move constructor - yet that constructor is marked explicit
, so such implicit construction should not be allowed.
As far as I can tell, this is a clang bug.
Copy-list-initialization has a rather unintuitive behaviour: It considers explicit constructors as viable until overload resolution is completely finished, but can then reject the overload result if an explicit constructor is chosen. The wording in a post-N4567 draft, [over.match.list]p1
In copy-list-initialization, if an
explicit
constructor is chosen, the initialization is ill-formed. [ Note: This differs from other situations (13.3.1.3, 13.3.1.4), where only converting constructors are considered for copy-initialization. This restriction only applies if this initialization is part of the final result of overload resolution. — end note ]
clang HEAD accepts the following program:
#include <iostream> using namespace std; struct String1 { explicit String1(const char*) { cout << "String1\n"; } }; struct String2 { String2(const char*) { cout << "String2\n"; } }; void f1(String1) { cout << "f1(String1)\n"; } void f2(String2) { cout << "f2(String2)\n"; } void f(String1) { cout << "f(String1)\n"; } void f(String2) { cout << "f(String2)\n"; } int main() { //f1( {"asdf"} ); f2( {"asdf"} ); f( {"asdf"} ); }
Which is, except for commenting out the call to f1
, straight from Bjarne Stroustrup's N2532 - Uniform initialization, Chapter 4. Thanks to Johannes Schaub for showing me this paper on std-discussion.
The same chapter contains the following explanation:
The real advantage of
explicit
is that it rendersf1("asdf")
an error. A problem is that overload resolution “prefers” non-explicit
constructors, so thatf("asdf")
callsf(String2)
. I consider the resolution off("asdf")
less than ideal because the writer ofString2
probably didn’t mean to resolve ambiguities in favor ofString2
(at least not in every case where explicit and non-explicit constructors occur like this) and the writer ofString1
certainly didn’t. The rule favors “sloppy programmers” who don’t useexplicit
.
For all I know, N2640 - Initializer Lists — Alternative Mechanism and Rationale is the last paper that includes rationale for this kind of overload resolution; it successor N2672 was voted into the C++11 draft.
From its chapter "The Meaning Of Explicit":
A first approach to make the example ill-formed is to require that all constructors (explicit and non-explicit) are considered for implicit conversions, but if an explicit constructor ends up being selected, that program is ill-formed. This rule may introduce its own surprises; for example:
struct Matrix { explicit Matrix(int n, int n); }; Matrix transpose(Matrix); struct Pixel { Pixel(int row, int col); }; Pixel transpose(Pixel); Pixel p = transpose({x, y}); // Error.
A second approach is to ignore the explicit constructors when looking for the viability of an implicit conversion, but to include them when actually selecting the converting constructor: If an explicit constructor ends up being selected, the program is ill-formed. This alternative approach allows the last (Pixel-vs-Matrix) example to work as expected (
transpose(Pixel)
is selected), while making the original example ("X x4 = { 10 };
") ill-formed.
While this paper proposes to use the second approach, its wording seems to be flawed - in my interpretation of the wording, it doesn't produce the behaviour outlined in the rationale part of the paper. The wording is revised in N2672 to use the first approach, but I couldn't find any discussion about why this was changed.
There is of course slightly more wording involved in initializing a variable as in the OP, but considering the difference in behaviour between clang and gcc is the same for the first sample program in my answer, I think this covers the main points.
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