使用braced-init列表调用显式构造函数:是否含糊不清?

时间:2016-01-05 22:06:38

标签: c++ c++11 gcc language-lawyer

请考虑以下事项:

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

brace(4)的构建清楚而明确地称(2)。在clang上,paren(3)的构造明确地调用了(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,所以这样不应该允许隐式构造。

2 个答案:

答案 0 :(得分:8)

据我所知,这是一个铿锵的错误

复制列表初始化具有相当不直观的行为:它将显式构造函数视为可行,直到重载完成完成,但是如果选择了显式构造函数,则可以拒绝重载结果。 N4567后草案中的措辞,[over.match.list] p1

  

在copy-list-initialization中,如果选择explicit构造函数,则为   初始化是不正确的。 [注意:这与其他不同   情境(13.3.1.3,13.3.1.4),其中只转换构造函数   被认为是复制初始化。此限制仅适用   如果这个初始化是最终过载结果的一部分   解析度。 - 结束记录]

clang HEAD接受以下程序:

#include <iostream>
using namespace std;

struct String1 {
    explicit String1(const char*) { cout << "String1\n"; }
};
struct String2 {
    String2(const char*) { cout << "String2\n"; }
};

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

除了评论f1的电话之外,直接来自Bjarne Stroustrup的N2532 - Uniform initialization,第4章。感谢Johannes Schaubstd-discussion上向我展示了这篇论文

同一章包含以下说明:

  

explicit的真正优势在于它呈现f1("asdf")   错误。一个问题是重载决策“更喜欢”非 - explicit   构造函数,以便f("asdf")调用f(String2)。我考虑一下   f("asdf")的分辨率低于理想因为作者   String2可能并不意味着解决有利于的歧义   String2(至少在每种情况下都不是明确的和非显式的   构造函数就像这样)和String1的作者当然   没有。该规则支持不使用explicit的“草率程序员”。

据我所知,N2640 - Initializer Lists — Alternative Mechanism and Rationale是最后一篇论文,其中包含了这种超载解决方案的基本原理;它的继任者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.
     

第二种方法是在查看时忽略显式构造函数   为了隐式转换的可行性,但在包含它们时   实际上选择转换构造函数:如果是显式的   构造函数最终被选中,程序格式不正确。这个   替代方法允许最后一个(Pixel-vs-Matrix)示例工作   按预期(transpose(Pixel)被选中),同时制作   原始例子(“X x4 = { 10 };”)形成不良。

虽然本文提议使用第二种方法,但其措辞似乎存在缺陷 - 在我对措辞的解释中,它不会产生本文理论部分所概述的行为。在N2672中对措辞进行了修订以使用第一种方法,但我找不到任何有关为何更改的讨论。

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

答案 1 :(得分:0)

即使评论太长,这也不是一个完整的答案 我会尝试为你的推理提出一个反例,我已经准备好了,因为我还远未确定。 无论如何,让我们试试!! : - )

它遵循简化的例子:

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临时对象,作为复制/移动构造函数的参数后立即使用,并且由于涉及的构造函数不明确而再次被允许

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

有意义吗?
只要您在我的推理中解释我的错误,我就准备删除答案。 : - )