8

我有一小段 C++11 代码,g++(4.7 或 4.8)拒绝编译,声称对 B2 b2a(x, {P(y)}) 的构造函数的调用不明确。Clang++ 对那个代码很满意,但拒绝编译 G++ 非常乐意编译的 B2 b2b(x, {{P(y)}}) !

两个编译器都对使用 {...} 或 {{...}} 作为参数的 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++ -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>> &);
    ^

克++:

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++ 对此相当严格),但我还不足以成为一名标准律师,无法解开应该是正确的行为这里!

4

1 回答 1

5

第一个代码,然后是我认为应该发生的事情。(在下文中,我将忽略第一个参数,因为我们只对第二个参数感兴趣。在您的示例中,第一个始终是完全匹配的)。请注意,规范中的规则目前在不断变化,所以我不会说一个或另一个编译器有错误。

B1 b1a(x, {y});

这段代码不能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}});

在这里,具有参数的构造函数的基本相同的参数计数计数const Y&

对于参数类型为 的构造函数const A<Y>&,则稍微复杂一些。重载解析中的转换成本规则计算初始化 an 的成本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)});

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

对于P<Y>参数构造函数,该参数将由 初始化{P<Y> object}。由于P<Y>没有初始化列表,列表将被拆分为单独的参数,并P<Y>使用 的右值对象调用 的移动构造函数P<Y>

对于A<P<Y>>参数构造函数,它与上面用A<Y>初始化的情况相同{y}: 由于std::initializer_list<P<Y>>可以用 初始化{P<Y> object},因此参数列表不会被拆分,因此大括号用于初始化该构造函数的 S 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}}可以初始化 a std::initializer_list<P<Y>>,因为{P<Y> object}可以转换为P<Y>(除了Y上面的 - dang,聚合)。

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


4人总结

  • 第二个构造函数调用成功
  • 假设std::initializer_list<T>是一个聚合,这很好并且成功调用第二个构造函数
  • 这里的歧义
  • 第二个构造函数调用成功
于 2013-07-17T21:33:31.560 回答