C++11构造函数重载解析和初始化列表:clang++与g++意见不一

8
我有一小段C++11代码,g++(4.7或4.8)拒绝编译并声称B2 b2a(x,{P(y)})的构造函数调用是模棱两可的。 Clang ++对该代码感到满意,但拒绝编译B2 b2b(x,{{P(y)}}),而g ++可以完美地编译它! 两个编译器都可以接受以{...}或{{...}}作为参数的B1构造函数。任何C ++语言专家都可以解释哪个编译器是正确的(如果有的话)以及发生了什么。下面是代码:
#include <initializer_list>

using namespace std;

class Y {};
class X;

template<class T> class P {
public:
    P(T);
};

template<class T> class A {
public:
    A(initializer_list<T>);
};

class B1 {
public:
    B1(const X&, const Y &);
    B1(const X&, const A<Y> &);
};

class B2 {
public:
    B2(const X &, const P<Y> &);
    B2(const X &, const A<P<Y>> &);
};

int f(const X &x, const Y y) {
    B1 b1a(x, {y});
    B1 b1b(x, {{y}});
    B2 b2a(x, {P<Y>(y)});
    B2 b2b(x, {{P<Y>(y)}});
    return 0;
}

同时也有编译器错误,clang:

$ clang++ -stdlib=libc++ -std=c++11 test-initialiser-list-4.cc -o test.o -c 
test-initialiser-list-4.cc:32:6: error: call to constructor of 'B2' is ambiguous
  B2 b2(x, {{P<Y>(y)}});
     ^  ~~~~~~~~~~~~~~
test-initialiser-list-4.cc:26:5: note: candidate constructor
    B2(const X &, const P<Y> &);
    ^
test-initialiser-list-4.cc:27:5: note: candidate constructor
    B2(const X &, const A<P<Y>> &);
    ^

g++:

test-initialiser-list-4.cc: In function 'int f(const X&, Y)':
test-initialiser-list-4.cc:32:21: error: call of overloaded 'B2(const X&, <brace-enclosed initializer list>)' is ambiguous
   B2 b2(x, {P<Y>(y)});
                     ^
test-initialiser-list-4.cc:32:21: note: candidates are:
test-initialiser-list-4.cc:27:5: note: B2::B2(const X&, const A<P<Y> >&)
     B2(const X &, const A<P<Y>> &);
     ^
test-initialiser-list-4.cc:26:5: note: B2::B2(const X&, const P<Y>&)
     B2(const X &, const P<Y> &);
     ^

这似乎涉及到统一初始化、初始化列表语法和带有模板参数的函数重载之间的交互(我知道g++对此要求相当严格),但我不是足够了解标准的律师,无法理解这里应该是正确的行为!


1
函数'f'中名称为'b2b'的两个本地变量只是一个笔误,不是问题的原因,我假设... - astraujums
1
在我看来,从initializer_list创建A和从T创建P应该都是精确匹配,因此存在歧义。我无法解释的是为什么编译器选择不同的内容进行投诉。 - Mark B
是的,我对其中一个(或两个!)选项含糊不清感到满意,但编译器对于应该选择哪个存在分歧,而我自己也不知道哪个是正确的。因此,我请求澄清。 - Phil Armstrong
1个回答

5

先编写代码,然后考虑我认为应该发生的事情。(在接下来的内容中,我将忽略第一个参数,因为我们只关心第二个参数。在您的示例中,第一个参数始终是精确匹配)。请注意,规则目前在规范中处于不稳定状态,因此我不能说哪个编译器有错误。

B1 b1a(x, {y});

在C++11中,这段代码无法调用const Y&构造函数,因为Y是一个聚合体并且Y没有类型为Y(当然)或其他可由其初始化的数据成员(这很丑陋,并正在努力解决 - C ++14 CD尚未对此进行措辞,因此我不确定最终的C ++14是否包含此修复)。

可以调用带有const A<Y>&参数的构造函数- {y}将作为A<Y>的构造函数的参数,并初始化该构造函数的std :: initializer_list <Y>

因此-成功调用第二个构造函数

B1 b1b(x, {{y}});

在这里,对于带有const Y&参数的构造函数,基本相同的论点适用。
对于参数类型为const A<Y>&的构造函数,情况会稍微复杂一些。在重载决议中计算初始化std::initializer_list<T>成本的转换成本规则要求大括号列表的每个元素都可以转换为T。然而,我们之前说过{y}不能转换为Y(因为它是一个聚合体)。现在重要的是要知道std::initializer_list<T>是否是一个聚合体。坦白地说,我不知道根据标准库条款是否必须将其视为聚合体。
如果我们认为它不是聚合体,那么我们将考虑std::initializer_list<Y>的复制构造函数,但是这又会触发完全相同的测试序列(导致重载决议检查中的“无限递归”)。由于这相当奇怪且不可实现,我认为任何实现都不会采取这种路径。
如果我们认为std::initializer_list是一个聚合体,我们将会说“没有找到转换”(参见上面的聚合体问题)。在这种情况下,由于我们无法将单个初始化程序列表作为整体调用初始化程序构造函数,{{y}}将被拆分成多个参数,并且A<Y>的构造函数将分别接受每个参数。因此,在这种情况下,我们将以{y}初始化std::initializer_list<Y>作为单个参数-这是完全正常的并且工作得很好。
因此,在假定std::initializer_list<T>是一个聚合体的情况下,这是可以的,并且可以成功调用第二个构造函数。
B2 b2a(x, {P<Y>(y)});
在这个案例和下一个案例中,我们不再像上面的Y那样存在聚合问题了,因为P<Y>有一个用户提供的构造函数。
对于P<Y>参数构造函数,该参数将被初始化为{P<Y>对象}。由于P<Y>没有初始化列表,因此该列表将被分成单个参数,并使用P<Y>的移动构造函数调用带有rvalue对象的P<Y>
对于A<P<Y>>参数构造函数,与上述情况的A<Y>相同,由{y}初始化:由于std::initializer_list<P<Y>>可以通过{P<Y>对象}进行初始化,参数列表不会被拆分,因此大括号用于初始化该构造函数的std::initializer_list<T>
现在,两个构造函数都很好用。它们在此处充当重载函数,并且它们的第二个参数在两种情况下都需要用户定义的转换。只有在两种情况下使用相同的转换函数或构造函数时,才能比较用户定义的转换序列——这种情况并不适用。 因此,在C ++11(以及C ++14 CD)中存在歧义。
请注意,这里有一个微妙的问题需要探讨。
struct X { operator int(); X(){/*nonaggregate*/} };

void f(X);
void f(int);

int main() {
  X x;
  f({x}); // ambiguity!
  f(x); // OK, calls first f
}

这个违反直觉的结果可能会在修复上述奇怪的聚合初始化问题时一并解决(两者都将调用第一个 f)。这是通过说 {x}->X 成为标识转换(X->x 也是如此)来实现的。目前,它是用户定义的转换。
因此,这里存在歧义。
B2 b2b(x, {{P<Y>(y)}});

对于带有参数const P<Y>&的构造函数,我们再次拆分参数并将{P<Y> object}参数传递给P<Y>的构造函数。请记住,P<Y>具有复制构造函数。但是这里的复杂之处在于我们不允许使用它(参见13.3.3.1p4),因为它需要用户定义的转换。唯一剩下的构造函数是采用Y,但Y不能通过{P<Y> object}进行初始化。
对于带有参数A<P<Y>>的构造函数,{{P<Y> object}}可以初始化一个std::initializer_list<P<Y>>,因为{P<Y> object}可以转换为P<Y>(与上面的Y不同 - 糟糕,聚合体)。
因此,第二个构造函数被成功调用。
所有4种情况的总结:
- 第二个构造函数被成功调用。 - 假设std::initializer_list<T>是一个聚合体,则这是可以的,并且可以成功调用第二个构造函数。 - 这里存在歧义。 - 第二个构造函数被成功调用。

谢谢,看起来我的困惑并不是完全没有道理的 :) - Phil Armstrong

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