c++ msvc和clang中的函数调用不明确,但gcc中没有

qni6mghb  于 2023-01-15  发布在  其他
关注(0)|答案(2)|浏览(177)

我想知道哪个编译器符合标准,我使用以下代码

#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和clang会产生一个编译器错误,而gcc可以处理这段代码。
https://godbolt.org/z/ETYGn5T1h
如果显式删除BaseImpl的复制ctor(uncomment(b)),所有三个编译器都可以正常运行。如果不显式删除(a)标准ctor(因为有用户定义的ctor,所以不会生成标准ctor),所有编译器都可以正常运行。
显然,clang和msvc认为,通过: BaseImpl({}),它们可以调用用户定义的ctor,或者间接地通过默认ctor调用隐式复制ctor。然而,由于删除了默认ctor,这种不明确性应该根本不存在。然而,通过显式删除默认ctor,编译器似乎首先假设BaseImpl存在默认ctor,并在检查它是否被删除之前生成一个错误。
我现在想知道这种行为是否符合标准。
编辑:我附上了编译器输出,以防不想或不能点击编译器资源管理器的链接:

vojdkbi0

vojdkbi01#

Clang和MSVC是正确的,gcc有一个bug。
将函数定义为已删除与不声明该函数不同。
除了移动构造函数、移动赋值函数和某些继承构造函数的情况(请参阅[over.match.funcs]/8),删除的函数被认为是为了解决重载问题而存在的。[over]节中没有其他内容特别处理删除的函数。我们有[over.best.ics]/2,强调我的:
隐式转换序列只涉及参数的类型、cv限定和值类别,以及如何转换这些属性以匹配参数的相应属性。[ * 注意:* 其他属性,如生存期、存储类、对齐、参数的可访问性、参数是否为位字段以及函数是否被删除将被忽略。因此,尽管可以为给定的参数-参数对定义隐式转换序列,但从参数到参数的转换在最终分析中仍可能是格式错误的。- end note ]
因此在impl() : BaseImpl({}) {}中,BaseImpl初始化器使用重载解析来选择BaseImpl构造函数,用于初始化基类子对象。候选者是BaseImpl的所有构造函数:预期的BaseImpl(const std::vector<std::string>&)、删除的BaseImpl()、隐式声明的复制构造函数BaseImpl(const BaseImpl&)以及隐式声明的(并且未删除)移动构造函数BaseImpl(BaseImpl&&)。此时,BaseImpl()不可行,因为初始化式只有一个参数。向量构造函数是可行的,因为存在一个构造函数vector(std::initializer_list<std::string>),它不是显式的,并且可以将{}参数转换为向量类型。也是可行的,因为BaseImpl()构造函数是声明的,并且不是显式的,并且“可以”将{}参数转换为BaseImpl类型。因此重载解析是不明确的,即使某些隐式转换序列使用已删除的函数。
BaseImpl() = delete;声明不存在时,BaseImpl就没有任何默认构造函数,因为BaseImpl(const std::vector<std::string>&)声明阻止默认构造函数的隐式声明,所以{}BaseImpl没有隐式转换序列。并且BaseImpl的复制和移动构造函数对于初始化BaseImpl({})是不可行的。向量构造函数是唯一可行的函数。
声明BaseImpl(BaseImpl&)时,无论是否删除,都将其视为复制构造函数(尽管缺少了通常的const),所以它防止了复制构造函数和移动构造函数的隐式声明。但是这个复制构造函数对于BaseImpl({})是不可行的,因为对非常数类型的引用不能绑定到使用BaseImpl()时涉及的右值临时BaseImpl对象(参见[over.ics.ref]/3)。因此只有预期的向量构造函数是可行的。

ttcibm8c

ttcibm8c2#

WIP标记

@aschepler的一条评论对这个结论提出了质疑。详情请看,如果你是语言律师,也可以参与进来。我今晚会仔细看看,然后更新这个答案。

TL; DR

GCC以一种非常不明显的方式是正确的。
∮发生了什么
clang错误消息看起来非常清楚:

<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

BaseImpl({})调用可以使用三个构造函数--生成的复制和移动构造函数,以及向量构造函数,Clang不知道选择哪一个。
MSVC的错误不那么直接:

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

这里发生的事情是MSVC试图调用复制或移动构造函数,为此它试图从初始化器{}创建一个BaseImpl类型的临时变量,并且这会失败,因为BaseImpl在它失败之前是抽象的,因为默认构造函数被删除了(所以你不会得到关于这一点的错误消息)。
GCC不考虑复制和移动ctors,即使它们是显式添加的,因此只是构造一个向量并编译良好。
∮应该发生什么
让我们深入了解一下这个标准,特别是[dcl.init.general],我将省略标准语言中不匹配的部分,并用(...)表示。
首先注意,根据[dcl. init. general](15)
15发生的初始化
(15.1)-对于带括号的表达式列表或带括号的初始化列表的初始化式,
(...)
称为直接初始化。
在[dcl.init.general](16)中有一长串初始化的条件,这里相关的是(16 - 6)。
(16.6)-否则,如果目标类型是一个(可能是cv限定的)类类型:
(...)
(16.6.2)-否则,如果初始化是直接初始化,或者如果它是复制初始化,其中源类型的cv非限定版本与目标类是同一个类,或者是目标类的派生类,则考虑构造函数。枚举适用的构造函数(12.4.2.4),并通过重载解析(12.4)选择最佳构造函数。然后:
(16.6.2.1)-如果重载解析成功,则调用选定的构造函数来初始化对象,初始化表达式或表达式列表作为其参数。
(...)
(16.6.2.3)-否则,初始化是病态的。
这归结起来就是:我们寻找所有适用的构造函数并尝试选择一个正确的。如果成功,我们就使用它,否则就是错误。
让我们看看[over. match. ctor]中的重载解析。
1当类类型的对象被直接初始化时(9.4),(...),重载解析选择构造函数。对于直接初始化或不在复制初始化上下文中的默认初始化,候选函数是被初始化对象的类的所有构造函数。(...)。参数列表是初始化式的表达式列表或赋值表达式。
因此,我们的候选函数集是生成的copy和move ctor以及vector ctor。下一步是根据[over. match. viable]检查哪些是可行的。这意味着首先检查调用中的参数数量是否适合候选函数(对所有候选函数为真),然后检查
[4]第三,对于可行函数F,每个自变量都存在一个隐式转换序列(12.4.4.2)将该实参转换为F的相应形参。如果形参具有引用类型,则隐式转换序列包括绑定引用的操作,并且对非常数的左值引用不能绑定到右值以及右值引用不能绑定到左值的事实可以影响函数的生存能力(参见12.4.4.2.5)。
根据[over.best.ics.general],隐式转换序列为:
3格式正确的隐式转换序列是以下形式之一:〉(3.1)-标准转换序列(12.4.4.2.2),
(3.2)- 用户定义的转换顺序(12.4.4.2.3),或
(3.3)- 省略号转换顺序(12.4.4.2.4)。
其中标准转换序列主要涉及int到long、lvalue到rvalue、ref到const ref等内容。这里我们对用户定义的转换序列感兴趣,它们是
1用户定义转换序列由一个初始标准转换序列、一个用户定义转换(11.4.8)和第二个标准转换序列组成。如果用户定义转换由构造函数(11.4.8.2)指定,则初始标准转换序列将源类型转换为该构造函数的第一个参数的类型。(...)
2.第二个标准转换序列将自定义转换的结果转换为序列的目标类型;任何引用绑定都包括在第二标准转换序列中(...)。
(...)

{}std::vector<std::string>肯定存在自定义的转换序列,由于删除了BaseImpl的默认构造函数,所以{}BaseImpl * 不 * 存在自定义的转换序列;这将需要两个用户定义的转换:一个连接到std::vector<std::string>,另一个连接到BaseImpl
因此,在三个候选构造函数中,只有std::vector<std::string>构造函数是可行的,符合重载解析的条件,应该被选择。GCC就是这么做的,除非我在分析中犯了错误,否则MSVC和Clang都有bug。

相关问题