调用带有支撑init列表的显式构造函数:是否不明确



考虑以下内容:

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)
}

CCD_ 1在CCD_ 2中的构造清楚而明确地调用CCD_。在clang上,(3)paren的构造明确地调用(1),其中与gcc 5.2一样,它无法使用:进行编译

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&&)

哪个编译器是对的?我怀疑clang在这里是正确的,因为gcc中的模糊性只能通过隐式构造B{1,2}并将其传递给复制/移动构造函数的路径出现,但该构造函数被标记为explicit,因此不应该允许这种隐式构造。

据我所知,这是一个clang bug

复制列表初始化有一个相当不直观的行为:它认为显式构造函数在重载解析完全完成之前是可行的,但如果选择了显式构造函数,则可以拒绝重载结果。N4567后草案中的措辞,[超过匹配列表]p1

在副本列表初始化中,如果选择了explicit构造函数,则初始化格式不正确。[注意:这与其他情况(13.3.1.3、13.3.1.4),其中仅转换构造函数被考虑用于复制初始化。此限制仅适用如果此初始化是过载最终结果的一部分决议。--尾注]


clang HEAD接受以下程序:

#include <iostream>
using namespace std;
struct String1 {
    explicit String1(const char*) { cout << "String1n"; }
};
struct String2 {
    String2(const char*) { cout << "String2n"; }
};
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"} );
}

这是,除了注释掉对brace0的调用之外,直接来自Bjarne Stroustrup的N2532-统一初始化,第4章。感谢Johannes Schaub给我看了这篇关于性病讨论的论文。

同一章包含以下解释:

CCD_ 11的真正优点是它使CCD_错误一个问题是过载解决方案"偏好"非explicit构造函数,使得f("asdf")调用f(String2)。我认为CCD_ 16的分辨率不理想是因为作者String2可能不是为了解决歧义而支持String2(至少不是在所有显式和非显式的情况下构造函数是这样出现的)和String1的作者没有。该规则支持那些不使用(4)0的"草率程序员"。


据我所知,N2640——初始化器列表——替代机制和原理是最后一篇包含这种过载解决原理的论文;它的继任者N2672被投票纳入C++11草案。

从其章节"显式的意义":

使示例格式不正确的第一种方法是要求构造函数(显式和非显式)被认为是隐式的转换,但如果最终选择了显式构造函数,那个程序格式不正确。这条规则可能会带来自己的惊喜;例如:

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.

第二种方法是在查找时忽略显式构造函数隐式转换的可行性,但在实际选择转换构造函数:如果显式构造函数最终被选中,程序格式不正确。这另一种方法允许最后一个(像素与矩阵)示例工作如预期(选择transpose(Pixel)),同时原始示例("X x4 = { 10 };")格式错误。

虽然本文建议使用第二种方法,但其措辞似乎有缺陷——在我对措辞的解释中,它没有产生论文基本原理部分概述的行为。N2672中的措辞被修改为使用第一种方法,但我找不到任何关于为什么要更改的讨论。


当然,与OP中一样,初始化变量所涉及的措辞稍多,但考虑到clang和gcc之间的行为差异与我回答中的第一个示例程序相同,我认为这涵盖了要点。

这不是一个完整的答案,尽管它作为注释太长了
我会尝试为你的推理提出一个反例,我准备投反对票,因为我还远不能确定
不管怎样,让我们试试!!:-)

它遵循简化的示例:

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

在这种情况下,声明{1, 2}显然给出了两个解决方案:

  • 通过B(A)直接初始化,因为A(int, int)不是显式的,因此它是允许的,这实际上是第一个候选

  • 出于同样的原因,它可以被解释为B{B(A{1,2})}(好吧,让我滥用这个符号来给你一个想法和我的意思),也就是说,{1,2}允许构造一个B临时对象,该对象在之后立即用作复制/移动构造函数的参数,并且它再次被允许,因为所涉及的构造函数不是显式

后者将解释第二个和第三个候选人。

这有道理吗
只要你向我解释我的推理错误,我就准备删除答案。:-)

最新更新