Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Calling an explicit constructor with a braced-init list: ambiguous or not?

Tags:

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.

like image 280
Barry Avatar asked Jan 05 '16 22:01

Barry


1 Answers

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 renders f1("asdf") an error. A problem is that overload resolution “prefers” non-explicit constructors, so that f("asdf") calls f(String2). I consider the resolution of f("asdf") less than ideal because the writer of String2 probably didn’t mean to resolve ambiguities in favor of String2 (at least not in every case where explicit and non-explicit constructors occur like this) and the writer of String1 certainly didn’t. The rule favors “sloppy programmers” who don’t use explicit.


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.

like image 179
dyp Avatar answered Oct 26 '22 22:10

dyp