When a type has both an explicit single-argument constructor and copy/move constructors, using an initializer list as an argument causes the compiler to throw this error:
error: call of overloaded 'Base(<brace-enclosed initializer list>)' is ambiguous
The detailed error message and code are as follows:
<source>: In function 'int main()':
<source>:33:23: error: call of overloaded 'Base(<brace-enclosed initializer list>)' is ambiguous
33 | Base b({a.shape()});
| ^
<source>:24:5: note: candidate: 'constexpr Base::Base(Base&&)'
24 | Base(Base &&) = default;
| ^~~~
<source>:23:5: note: candidate: 'constexpr Base::Base(const Base&)'
23 | Base(const Base &) = default;
| ^~~~
<source>:22:14: note: candidate: 'Base::Base(const Layout&)'
22 | explicit Base(const Layout &layout) : m_layout(layout) {}
| ^~~~
Compiler returned: 1
#include <utility>
#include <vector>
#include <cstddef>
using IntTuple = std::vector<size_t>;
class Layout {
IntTuple m_shape;
public:
Layout(const IntTuple &shape) : m_shape(shape) {}
IntTuple shape() {
return m_shape;
}
};
class Base {
Layout m_layout;
public:
explicit Base(const Layout &layout) : m_layout(layout) {}
Base(const Base &) = default;
Base(Base &&) = default;
IntTuple shape() {
return m_layout.shape();
}
};
int main() {
Base a(IntTuple{1, 2, 3});
Base b({a.shape()}); // error
}
In my understanding, the explicit
keyword should have disabled the implicit conversion path from the initializer list to the Base type, so this error should not occur.
This error is observed in GCC versions below 13.2, while it compiles successfully in versions 13.2, 14.1 and above. I would like to know the reason why GCC 13.1 fails to compile this. (EDITED)
I am aware that Overloaded call is ambiguous: one-pair inline map as constructor argument raises a similar issue. However, there is a subtle difference between the example code in that case and this one. In that example, there are indeed two overload resolution paths, whereas in this case, the use of explicit
restricts it to only one viable path in principle. Additionally, the link does not explain why the code compiles successfully in GCC versions 14.1 and above, nor does modifying Base b({a.shape()});
to Base b({{a.shape()}});
resolve the issue. Therefore, I believe this problem still warrants further discussion.
By looking through the version history on GitHub, it's possible to identify the commit that caused this change in behaviour between GCC 13.1 and 13.2: c++: fix explicit/copy problem [PR109247]
Basically, the rule is that when doing copy-list-initialization, explicit constructors are considered, but you end up getting an ill-formed program if the actually chosen constructor is explicit. This differs from the behaviour of copy-non-list-initialization, which is that explicit constructors aren't candidates.
This rule can cause surprising outcomes, such as in OP's example. The problem is that Base(const Base&)
and Base(Base&&)
are candidates because it is possible to form an implicit conversion sequence from {a.shape()}
to Base
, which the reference then binds to, even though those implicit conversion sequences would be ill-formed because they use the explicit constructor. It's also worthwhile to state exactly what happens in these implicit conversion sequences, because we will need this information later on to explain why the top-level overload resolution is ambiguous. The (ill-formed) copy-initialization of Base
from {a.shape()}
works by calling the constructor Base(const Layout&)
, where a.shape()
is implicitly converted to const Layout&
. In other words the implicit conversion sequence is:
a.shape()
to const Layout&
;Base(const Layout&)
(ill-formed because explicit);const Base&
or Base&&
) to resulting Base
temporaryIf Base(const Layout&)
were used directly for the initialization then the implicit conversion sequence that it would use is
a.shape()
to const IntTuple&
(exact match);Layout(const IntTuple&)
;const Layout&)
to resulting Layout
temporaryYou may have noticed that this entire 3-step implicit conversion sequence forms the first step of the other two implicit conversion sequences, the ones that would be needed in order to call Base(const Base&)
or Base(Base&&)
. So you would think that this would make the implicit conversion sequences to const Base&
and Base&&
worse than the implicit conversion sequence to const Layout&
(implying no ambiguity) because they are doing "strictly more".
But the rules for comparing user-defined implicit conversion sequences are not that smart. What they say is that you have to write the implicit conversion sequences in the three-step form shown above (where the second step is to call the constructor or conversion function that produces a type that can be converted to the final parameter type using a standard conversion, the third step) and then, if the second step in each implicit conversion sequence calls the same function, you can compare them based on which one has a better standard conversion sequence in step 3. (There is also a special rule about aggregate initialization that isn't relevant here.) See [over.ics.rank]/3.3. If this rule cannot be applied then neither implicit conversion sequence is better than the other. The fact that one implicit conversion sequence might be ill-formed does not affect the ranking algorithm.
In GCC 13.2 a new rule was introduced that attempts to break this ambiguity and try to "do the right thing" in cases including the OP's case. It is the proposed rule in CWG2735. The idea is to explicitly penalize the copy and move constructors if they are viable only through an implicit conversion sequence that would (illegally) use an explicit constructor.
The new behaviour is closer to where the standard is expected to "end up", but CWG2735 is not resolved yet so we don't know for sure.