使用花括号初始化列表调用显式构造函数:是否模棱两可?

23

请考虑以下内容:

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)
}
在(4)中,brace的构造清晰明确地调用了(2)。在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,因此不应允许进行这种隐式构造。


2
MSVS 2015 也可以编译这个。 - NathanOliver
2
这看起来非常类似于这里的问题描述:https://gcc.gnu.org/ml/gcc-help/2014-02/msg00004.html,导致http://gcc.gnu.org/bugzilla/show_bug.cgi?id=60027,目前仍未解决/评论。 - kfunk
1
有趣。Copy-list-init 应该考虑所有构造函数,如果选择了显式构造函数,则应该是非法的。问题是这是否意味着不能形成隐式转换序列,还是可以形成隐式转换序列,然后才会出现非法性。在第一种情况下,它将是明确的;在第二种情况下,它是不明确的。 - T.C.
2
@T.C. 在 [over.match.list]p1 中有一个与此相关的注释:“仅当此初始化是重载决策的最终结果的一部分时,才适用此限制。”如果我理解正确,这意味着它是模棱两可的。 - dyp
4
我认为这是CWG 1228 - dyp
显示剩余8条评论
2个回答

11
据我所知,这是一个clang的bug
复制列表初始化具有相当不直观的行为:它认为显式构造函数在重载解析完全完成之前是可行的,但如果选择了显式构造函数,则可以拒绝重载结果。根据N4567草案中的措辞,[over.match.list]p1

在复制列表初始化中,如果选择了一个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草案。

从其章节“explicit的含义”中可以得知:

使示例代码非法的第一种方法是要求对隐式转换考虑所有构造函数(显式和非显式),但如果选择了显式构造函数,则该程序不合规。这条规则可能引入其自身的意外情况;例如:

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中修订了措辞以使用第一种方法,但我找不到任何关于为什么要更改的讨论。
当然,在初始化变量方面还涉及些许措辞,但考虑到clang和gcc之间在我的答案中的第一个示例程序中的行为差异相同,我认为这涵盖了主要观点。

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临时对象,该对象立即用作复制/移动构造函数的参数,并且再次允许,因为涉及的构造函数不是显式的

后者可以解释为第二个和第三个候选项。

这有道理吗?
只要您向我解释我的推理中哪里有问题,我就准备删除答案。 :-)


2
第二个违反了一个用户自定义转换规则。 - T.C.
标准语散落在各个地方,但基本思想是,在形成转换序列时,最多只能使用一个用户定义的转换。 - T.C.
@T.C.明白了,它违反了规则,因为隐式声明的复制构造函数和移动构造函数也是转换构造函数,我说得对吗? - skypjack
@T.C. 我还是错了,文档上说一个没有声明为explicit的构造函数,并且可以用单个参数调用(直到C++11)被称为转换构造函数。但事实并非如此。我不明白为什么会违反这个规则... :-( ...很抱歉打扰您,但我想理解一下。 - skypjack
无论如何,它都会调用B(B{1,2}),因为正如@dyp所说,显式构造函数被认为是候选列表的一部分。只有当最终选择了一个显式构造函数时,程序才会出现错误。由于存在歧义(三个可能的构造函数,所有这些都需要用户转换),因此没有一个被选中,而是显示了这三个候选项。 - ABu
显示剩余2条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接