Home > OS >  Ambiguous function call in msvc and clang but not in gcc
Ambiguous function call in msvc and clang but not in gcc

Time:01-11

I wonder which compiler is compliant with the standard, i use the following code

#include <iostream>
#include <string>
#include <memory>
#include <vector>


class AbstractBase
{
public:
    virtual ~AbstractBase() {};
    virtual std::string get_name() = 0;
    virtual int get_number() = 0;
};

class BaseImpl : public AbstractBase
{
public:
    BaseImpl() = delete;             //(a)
    //BaseImpl(BaseImpl&) = delete;  //(b)
    BaseImpl(const std::vector<std::string>& name_) : name(name_) {}
    std::string get_name() override {return name.empty() ? std::string("empty") : name.front();}
private:
    std::vector<std::string> name{};
};

class impl : public BaseImpl
{
public:
    impl() : BaseImpl({}) {} 
    int get_number() override {return 42;} 
};

int main()
{
    std::unique_ptr<AbstractBase> intance = std::make_unique<impl>();
    std::cout << intance->get_name() << " " << intance->get_number() << "\n";
    return 0;
}

msvc and clang produce a compiler error while gcc is fine with this code. Link to godbold

screenshot of compiler output

CodePudding user response:

Clang and MSVC are correct, and gcc has a bug.

Defining a function as deleted is not the same as not declaring the function.

Except for move constructors, move assignment functions, and some cases of inherited constructors (see [over.match.funcs]/8), a deleted function is considered to exist for purposes of overload resolution. Nothing else in section [over] treats a deleted function specially. And we have [over.best.ics]/2, emphasis mine:

Implicit conversion sequences are concerned only with the type, cv-qualification, and value category of the argument and how these are converted to match the corresponding properties of the parameter. [ Note: Other properties, such as the lifetime, storage class, alignment, accessibility of the argument, whether the argument is a bit-field, and whether a function is deleted, are ignored. So, although an implicit conversion sequence can be defined for a given argument-parameter pair, the conversion from the argument to the parameter might still be ill-formed in the final analysis. — end note ]

So within impl() : BaseImpl({}) {}, the BaseImpl initializer uses overload resolution to select the BaseImpl constructor used to initialize the base class subobject. The candidates are all the constructors of BaseImpl: the intended BaseImpl(const std::vector<std::string>&), the deleted BaseImpl(), the implicitly declared copy constructor BaseImpl(const BaseImpl&), and the implicitly declared (and not deleted) move constructor BaseImpl(BaseImpl&&). At this point, BaseImpl() is not viable since the initializer has one argument. The vector constructor is viable since there is a constructor vector(std::initializer_list<std::string>) which is not explicit and can convert the {} argument to the vector type. The copy and move constructors are also viable since the BaseImpl() constructor is declared, and is not explicit, and "can" convert the {} argument to type BaseImpl. So overload resolution is ambiguous, even though some of the implicit conversion sequences use a deleted function.

When the BaseImpl() = delete; declaration is not present, BaseImpl simply doesn't have any default constructor, since the BaseImpl(const std::vector<std::string>&) declaration prevents implicit declaration of a default constructor. So there is no implicit conversion sequence for {} to BaseImpl, and the copy and move constructors of BaseImpl are not viable for the initialization BaseImpl({}). The vector constructor is the only viable function.

When you declare BaseImpl(BaseImpl&), deleted or not, this is considered a copy constructor (despite missing the usual const), so it prevents the implicit declarations of the copy constructor and move constructor. But this copy constructor is not viable for BaseImpl({}), since the reference to non-const type can't bind to the rvalue temporary BaseImpl object involved in using BaseImpl() (see [over.ics.ref]/3). So only the intended vector constructor is viable.

CodePudding user response:

TL;DR

GCC is correct in a quite non-obvious way.

What is happening

The clang error message seems pretty clear:

<source>:28:14: error: call to constructor of 'BaseImpl' is ambiguous
    impl() : BaseImpl({}) {}
             ^        ~~
<source>:14:7: note: candidate constructor (the implicit move constructor)
class BaseImpl : public AbstractBase
      ^
<source>:14:7: note: candidate constructor (the implicit copy constructor)
<source>:19:5: note: candidate constructor
    BaseImpl(const std::vector<std::string>& name_) : name(name_) {}
    ^
1 error generated.
Compiler returned: 1

There are three constructors that are available for the BaseImpl({}) call -- the generated copy and move constructors, and the vector constructor. Clang does not know which of these to choose.

MSVCs error is a bit less straightforward:

x64 msvc v19.latest (Editor #1)

x64 msvc v19.latest
x64 msvc v19.latest
/std:c  20
123
<Compilation failed>

# For more information see the output window
x64 msvc v19.latest - 2681ms
Output of x64 msvc v19.latest (Compiler #1)
example.cpp
<source>(28): error C2259: 'BaseImpl': cannot instantiate abstract class
<source>(14): note: see declaration of 'BaseImpl'
<source>(28): note: due to following members:
<source>(28): note: 'int AbstractBase::get_number(void)': is abstract
<source>(11): note: see declaration of 'AbstractBase::get_number'
Compiler returned: 2

What happens here is that MSVC attempts to call the copy or move constructor, that for this it attempts to create a temporary of type BaseImpl from the initializer {}, and that this fails because BaseImpl is abstract before it fails because the default constructor is deleted (so you don't get an error message about that).

GCC does not consider the copy and move ctors even if they are explicitly added and so just constructs a vector and compiles fine.

What should happen

Let's dive into the standard. In particular, let's have a look at [dcl.init.general]. I'll omit non-matching parts of the standard language and denote that with (...).

First note that according to [dcl.init.general] (15)

15 The initialization that occurs

(15.1) — for an initializer that is a parenthesized expression-list or a braced-init-list,

(...)

is called direct-initialization.

There follows in [dcl.init.general] (16) a long list of conditions for what happens in initialization. The relevant here is (16.6)

(16.6) — Otherwise, if the destination type is a (possibly cv-qualified) class type:

(...)

(16.6.2) — Otherwise, if the initialization is direct-initialization, or if it is copy-initialization where the cv-unqualified version of the source type is the same class as, or a derived class of, the class of the destination, constructors are considered. The applicable constructors are enumerated (12.4.2.4), and the best one is chosen through overload resolution (12.4). Then:

(16.6.2.1) — If overload resolution is successful, the selected constructor is called to initialize the object, with the initializer expression or expression-list as its argument(s).

(...)

(16.6.2.3) — Otherwise, the initialization is ill-formed.

What this boils down to is: we look for all applicable constructors and attempt to choose a correct one. If that works, we use it, otherwise it's an error.

So let's take a look at overload resolution in [over.match.ctor]. Here it states that

1 When objects of class type are direct-initialized (9.4), (...), 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. (...). The argument list is the expression-list or assignment-expression of the initializer.

So our set of candidate functions are the generated copy and move ctors as well as the vector ctor. Next step is checking which of these are viable according to [over.match.viable]. This means first checking that the number of arguments in the call fits the candidate functions (true for all candidates) and then that

4 Third, for F to be a viable function, there shall exist for each argument an implicit conversion sequence (12.4.4.2) that converts that argument to the corresponding parameter of F. If the parameter has reference type, the implicit conversion sequence includes the operation of binding the reference, and the fact that an lvalue reference to non-const cannot be bound to an rvalue and that an rvalue reference cannot be bound to an lvalue can affect the viability of the function (see 12.4.4.2.5).

An implicit conversion sequence is, according to [over.best.ics.general],

3 A well-formed implicit conversion sequence is one of the following forms: > (3.1) — a standard conversion sequence (12.4.4.2.2),

(3.2) — a user-defined conversion sequence (12.4.4.2.3), or

(3.3) — an ellipsis conversion sequence (12.4.4.2.4).

where a standard conversion sequence is chiefly concerned with stuff like int to long, lvalue to rvalue, ref to const ref etc. We're interested in user-defined conversion sequences here, which are

1 A user-defined conversion sequence consists of an initial standard conversion sequence followed by a user- defined conversion (11.4.8) followed by a second standard conversion sequence. If the user-defined conversion is specified by a constructor (11.4.8.2), the initial standard conversion sequence converts the source type to the type of the first parameter of that constructor. (...)

2 The second standard conversion sequence converts the result of the user-defined conversion to the target type for the sequence; any reference binding is included in the second standard conversion sequence. (...).

(...)

There is quite definitely a user-defined conversion sequence from {} to std::vector<std::string>. Because BaseImpl's default constructor is deleted, there is not a user-defined conversion sequence from {} to BaseImpl; this would require two user-defined conversions: one to std::vector<std::string> and another to BaseImpl.

So of the three candidate constructors, only the std::vector<std::string> constructor is viable and eligible for overload resolution and should be chosen. GCC does this, and unless I made an error in my analysis, MSVC and clang have a bug.

  • Related