C++ 11构造函数重载决策和initialiser_lists:clang ++和g ++不同意

Phi*_*ong 8 c++ templates constructor-overloading ambiguous c++11

我有一小段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;
}
Run Code Online (Sandbox Code Playgroud)

和编译器错误,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>> &);
    ^
Run Code Online (Sandbox Code Playgroud)

克++:

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> &);
     ^
Run Code Online (Sandbox Code Playgroud)

这有点像统一初始化,初始化列表语法和函数重载与模板化参数之间的相互作用(我知道g ++是相当严格的),但我不足以成为标准律师能够解包应该是正确的行为这里!

Joh*_*itb 5

第一个代码,那么我认为应该发生什么.(在下文中,我将忽略第一个参数,因为我们只对第二个参数感兴趣.第一个参数在您的示例中总是完全匹配).请注意,规范目前在规范中有所不同,所以我不会说一个或另一个编译器有错误.

B1 b1a(x, {y});
Run Code Online (Sandbox Code Playgroud)

这段代码不能const Y&在C++ 11中调用构造函数,因为它Y是一个聚合,Y并且没有类型的数据成员Y(当然)或者其他可以初始化的东西(这是丑陋的东西,并且可以修复 - C + +14 CD还没有这方面的措辞,所以我不确定最终的C++ 14是否会包含此修复程序).

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

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

B1 b1b(x, {{y}});
Run Code Online (Sandbox Code Playgroud)

这里,基本相同的参数计算带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}a初始化std::initializer_list<Y>为单个参数 - 这非常精细并且像魅力一样工作.

因此,假设std::initializer_list<T>是一个聚合,这很好,并成功调用第二个构造函数.

B2 b2a(x, {P<Y>(y)});
Run Code Online (Sandbox Code Playgroud)

在这种情况下和下一种情况下,我们不再具有上面的聚合问题Y,因为P<Y>它具有用户提供的构造函数.

对于P<Y>参数构造函数,该参数将由初始化{P<Y> object}.由于P<Y>没有初始化列表,列表将被拆分为单独的参数,P<Y>并使用rvalue对象调用move-constructor P<Y>.

对于A<P<Y>>参数构造函数,它与上面的情况相同,A<Y>初始化为{y}:由于std::initializer_list<P<Y>>可以初始化{P<Y> object},因此参数列表不会被拆分,因此大括号用于初始化该构造函数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
}
Run Code Online (Sandbox Code Playgroud)

这个反直观的结果可能会在修复上面提到的聚合初始化怪异的同一次运行中修复(两者都将调用第一个f).这是通过说{x}->X成为身份转换(按原样X->x)来实现的.目前,它是用户定义的转换.

所以,这里含糊不清.

B2 b2b(x, {{P<Y>(y)}});
Run Code Online (Sandbox Code Playgroud)

对于带参数的构造函数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}}可以初始化a std::initializer_list<P<Y>>,因为{P<Y> object}可以转换为P<Y>(除了Y上面 - dang,聚合).

所以,第二个构造函数调用成功.


所有4的总结

  • 第二个构造函数成功调用
  • 在假设std::initializer_list<T>是聚合的情况下,这很好,并成功调用第二个构造函数
  • 这里含糊不清
  • 第二个构造函数成功调用