Ambiguous constructor error in gcc but not in msvc
Asked Answered
S

2

10

I recently learnt that we can have more than one default constructor in a class. Then I wrote the following program that compiles with msvc but both clang and gcc fails to compile this.

struct A
{
  explicit A(int = 10);
  A()= default;
};

A a = {}; //msvc ok but gcc and clang fails here

Live demo

I want to know which compiler is correct here as per the C++17 standard.

GCC says:

<source>:8:8: error: conversion from '<brace-enclosed initializer list>' to 'A' is ambiguous
    8 | A a = {}; //msvc ok but gcc and clang fails here
      |        ^
<source>:5:3: note: candidate: 'constexpr A::A()'
    5 |   A()= default;
      |   ^
<source>:4:12: note: candidate: 'A::A(int)'
    4 |   explicit A(int = 10);
Stereotypy answered 9/1 at 18:4 Comment(15)
Can you include the exact error or warning text for reference? It'll also be necessary to explain how you compiled this, as in with what flags, as different C++ standards may behave differently.Spike
Is is supposed to initialized with the parameter-less default constructor or the constructor with the defaulted parameter? Inquring compilers want to know.Obidiah
That's a very good question. How could you tell the difference between those two constructors?Patino
Quickie Compiler Explorer demoObidiah
@TimRoberts Looks like msvc got it right this time since other is explicit.Collen
looks like MSVC considers that the explicit constructor as not called since {} is an implicit object creation. not sure which is correct hereForeglimpse
note that if you remove the equals sign, msvc stops accepting this too.Cording
Funny how the GCC also assumed the constructor to be implicitly constexpr qualified, thereby avoiding another clash in the first place.Antrim
@n.m.couldbeanAI That is because for direct initialization, all ctors are candidates and so the ambiguity as explained in the last quoted reference in my asnwer. Note also that on the other hand for copy-initialization there is no such ambiguity and so this compiles.Collen
Very interesting bug and nice repro. I've submitted an LLVM bug: github.com/llvm/llvm-project/issues/77507Frustrate
Having an explicit constructor with a default parameter is nonsensical to begin with, I'm not surprised you're having trouble with it.Jamisonjammal
Apparently, the Microsoft compiler does not "see" the default parameter 10, unless there is an attempt to invoke that constructor. All it sees is explicit A(int);, which, of course, does not conflict with A(). If, however, you attempt to invoke the default constructor (without the legerdemain of copy-initialization), then MSVC notices that there are two default constructors.Gidgetgie
So, if function main defines a variable like this: A b;, you will get a duplicate definition error. This rubs me wrong. I would like the compiler to emit an error whether or not any objects of class A are declared. There are two default constructors, and I would like the compiler to say something about that.Gidgetgie
I added two test functions to class A: void test(int = 10) {} and void test() {} If you call the parameterless version, test(), you will get a duplicate definition error. If, however, you always supply an integer argument, the two functions compile in MSVC, and the program runs. Just like the constructors, MSVC does not see the default parameter value 10, unless you force it to.Gidgetgie
This is CWG 2856Collen
C
7

TLDR;

This is CWG 2856 and the program is well-formed as per the current wording in the standard as descirbed below. Basically, only one of the ctors(A::A() and explicit A::A(int)) is a converting constructor. Thus only the former A::A() is the viable option and can be used.


First note that A a = {}; is copy-initialization.

  1. The initialization that occurs in the = form of a brace-or-equal-initializer or condition ([stmt.select]), as well as in argument passing, function return, throwing an exception ([except.throw]), handling an exception ([except.handle]), and aggregate member initialization ([dcl.init.aggr]), is called copy-initialization.

Next we move to the semantics of the initializer.

  1. The semantics of initializers are as follows. The destination type is the type of the object or reference being initialized and the source type is the type of the initializer expression. If the initializer is not a single (possibly parenthesized) expression, the source type is not defined.
  • If the initializer is a (non-parenthesized) braced-init-list or is = braced-init-list, the object or reference is list-initialized ([dcl.init.list]).

The above means that, the object is to be list-initialized.

From list initialization:

List-initialization is initialization of an object or reference from a braced-init-list. Such an initializer is called an initializer list, and the comma-separated initializer-clauses of the initializer-list or designated-initializer-clauses of the designated-initializer-list are called the elements of the initializer list. An initializer list may be empty. List-initialization can occur in direct-initialization or copy-initialization contexts; list-initialization in a direct-initialization context is called direct-list-initialization and list-initialization in a copy-initialization context is called copy-list-initialization.

The above means that A a = {}; is copy-list initiaization. Next we see the effect of list initialization:

  1. List-initialization of an object or reference of type T is defined as follows:
  • Otherwise, if the initializer list has no elements and T is a class type with a default constructor, the object is value-initialized.

The above means that the object will be value initialized, so we move on to value-initialization:

  1. To value-initialize an object of type T means:
  • if T has either no default constructor ([class.default.ctor]) or a default constructor that is user-provided or deleted, then the object is default-initialized;

This means that the object will be default initialized:

  1. To default-initialize an object of type T means:
  • If T is a (possibly cv-qualified) class type ([class]), constructors are considered. The applicable constructors are enumerated ([over.match.ctor]), and the best one for the initializer () is chosen through overload resolution ([over.match]). The constructor thus selected is called, with an empty argument list, to initialize the object.

So we move on to over.match.ctor to get a list of candidate ctors. Also do note the initializer() part in the above quoted reference as it will be used at the end as an argument.

  1. When objects of class type are direct-initialized, copy-initialized from an expression of the same or a derived class type ([dcl.init]), or default-initialized, overload resolution selects the constructor. For direct-initialization or default-initialization that is not in the context of copy-initialization, the candidate functions are all the constructors of the class of the object being initialized. For copy-initialization, the candidate functions are all the converting constructors of that class. The argument list is the expression-list or assignment-expression of the initializer.

This means that only the non-explicit ctor A::A() is the candidate because it is a converting ctor while the other explicit A::A(int) is not. Thus the set of candidates and viable options only contains one ctor A::A().

Finally, since we only have one viable option, it is the one that is selected for the initializer ().

Collen answered 9/1 at 18:18 Comment(11)
@273K A::A(int) is not even a candidate because it was rejected as per over.match.ctor since it is not a converting ctor. Thus, the set of candidates contains only one ctor A::A() which is the only one considered and also viable.Collen
Yes, it's not a converting constructor, but A::A(int = 10) is still a default constructor.Crocus
@273K the crazy thing is that if you remove the second constructor, the code fails to compile because A(int) cannot be called. If you leave it in, the code fails to compile because A(int) can be called and overload resolution is ambiguous. This is some quantum superstate nonsense where A(int) is viable depending on how you look at it :) This may be my favorite compiler bug so far.Frustrate
@273K Yes, but that(A::A(int)) is not considered because it was eliminated. It cannot reenter the set once it has been removed.Collen
So, A a = {}; is not always the same as A a{};.Crocus
@273K Yes, seems like the intention(as per the wording) i.e., the wording seems to suggest that.Collen
You chain of reasoning is fine almost all the way to the end, but didn't you get that "non-explicit ctor A::A() is the candidate because it is a converting ctor" part exactly the wrong way around? A::A(int) is the only converting constructor, and therefor the only surviving candidate.Antrim
But from there on you have to go one or two steps more, where explicit constructors are once again explicitly forbidden for copy-list-initialization.Antrim
timsong-cpp.github.io/cppwp/n4659/class.conv.ctor#2 " An explicit constructor constructs objects just like non-explicit constructors, but does so only where the direct-initialization syntax or where casts ([expr.static.cast], [expr.cast]) are explicitly used" - note how that required direct initialization?Antrim
@Ext3h: Definition of "converting constructor" was dramatically changed by C++11 (when initializer lists were introduced into general syntax), they went from "can be called with exactly one parameter" to not caring about the number of parameters, because an initializer list can have any number. But the definition does exclude explicit candidates.Sclerotomy
Even really simple things are really hard to understand in modern C++. Not even the compiler builders understand the language any longer. Does not bode well for usability.Myna
F
0

@user12002570's answer already covers most of the standardese, but to summarize and expand upon it:

A a = {}

Here comes the critical part:

For copy-initialization (including default initialization in the context of copy-initialization), the candidate functions are all the converting constructors of that class.

- C++ working draft, [over.match.ctor] p1

The clarification in bold did not exist in the C++17 standard, but has been added in Editorial Issue 1911. Such editorial changes are considered to apply retroactively to older standards as well. Without the clarification, it's not obvious whether the default-initialization in {} should have converting constructors as candidates. That's likely why there is so much implementation divergence in this case.

{} is default-initialization in the context of copy-initialization, and only converting constructors should be in the candidate set. A(int) is not a converting constructor because it is explicit. Therefore, your program is well-formed.


Note: I have submitted an LLVM bug 77507 about this.

Frustrate answered 10/1 at 11:39 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.