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

2

25

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.

Milkwort answered 5/1, 2016 at 22:6 Comment(13)
MSVS 2015 will also compiles this.Barrada
This looks very similar to the problem description here: gcc.gnu.org/ml/gcc-help/2014-02/msg00004.html which led to gcc.gnu.org/bugzilla/show_bug.cgi?id=60027 which is unresolved/uncommented so far.Oconnell
Interesting. Copy-list-init is supposed to consider all constructors, and be ill-formed if an explicit constructor is chosen. The question is whether this means an implicit conversion sequence cannot be formed, or if it can be formed and the ill-formedness then comes in. In the first case it would be unambiguous; in the second case it's ambiguous.Flavourful
@Flavourful There's a comment related to this in [over.match.list]p1 "This restriction only applies if this initialization is part of the final result of overload resolution." If I understand it correctly, this suggests that it is ambiguous.Baeda
I think this is CWG 1228Baeda
@Baeda The NAD disposition of that issue doesn't necessarily imply CWG's agreement with the interpretation of the example in it, though.Flavourful
@Baeda Yeah that's the same. How is that "as intended"?Milkwort
@Flavourful Sorry, but I don't understand what you mean with "agreement with the interpretation". Are you referring to what the code does (from a high-level perspective), what it should do (which would contradict the explicit "as intended"), or something else?Baeda
@Baeda The disposition says that the "rules [for selecting candidate functions] are as intended". It doesn't say that these rules will lead to that code example being ambiguous.Flavourful
Is this still an open question or do you consider it answered based on the comments? I recently filed a bug report for MSVC - it treats copy-list-initialization like non-list copy-initialization with regards to explicit, which makes it compile your example - and this made me look at these issues in detail. For what it's worth, I agree with @Baeda that the Standard wording as it currently stands makes this ambiguous.Zwickau
@Zwickau I guess an answer along those lines would be good?Milkwort
I've sent an email some days ago to Daniel Krügler, asking if he might clarify CWG's response to CWG 1228 (as pointed out by @T.C.) but received no answer yet.Baeda
Five years later - the notes from CWG discussion on 1228 (which ended up being a single line) confirms that the issue's interpretation of the rules is correct.Flavourful
B
11

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.

Baeda answered 17/1, 2016 at 14:19 Comment(0)
C
0

This is not a complete answer, even though it is too long as a comment.
I'll try to propose a counterexample to your reasoning and I'm ready to see downvote for I'm far from being sure.
Anyway, let's try!! :-)

It follows the reduced example:

struct A {
    A(int, int) { }
};

struct B {
    B(A) { }
    explicit B(int, int ) { }
};

int main() {
    B paren({1, 2});
}

In this case, the statement {1, 2} gives place apparently to two solutions:

  • direct initialization by means of B(A), because A(int, int) is not explicit and thus it is allowed and that's actually the first candidate

  • for the same reason above, it can be interpreted as B{B(A{1,2})} (well, let me abuse the notation to give you an idea and what I mean), that is {1,2} allows the construction of a B temporary object that is used immediately after as an argument for the copy/move constructor, and it's allowed again because the involved constructors are not explicit

The latter would explain the second and the third candidates.

Does it make sense?
I'm ready to delete the answers as long as you explain me what's wrong in my reasoning. :-)

Carlos answered 6/1, 2016 at 0:10 Comment(7)
The second one breaks the one-user-defined-conversion rule.Flavourful
I trust you, but can you be more detailed? I suspect I'm to learn something new here. Thank you.Carlos
The standardese is scattered all over the place, but the basic idea is that in forming a conversion sequence you can use at most one user-defined conversion.Flavourful
@Flavourful Got it, it breaks the rule because implicitly declared copy ctors and move ctors are converting constructors too, am I right?Carlos
@Flavourful I'm still wrong, the documentation says A constructor that is not declared with the specifier explicit and which can be called with a single parameter (until C++11) is called a converting constructor.. This is not the case. I don't manage to see why it breaks that rule... :-( ... I'm sorry, I don't want to bother you, but I'd like to understand.Carlos
@Carlos cppreference uses half-open ranges: [since, until).Flavourful
In any case, it would call B(B{1,2}), because, as @Baeda says, the explicit constructor are consider to form the list of candidates. Only if a explicit constructor is finally selected, the program is ill-formed. Since there's an ambiguity (three posible constructors, all of them requiring a user-conversion), no one is selected and the three candidates are shown.Mientao

© 2022 - 2024 — McMap. All rights reserved.