【问题标题】:C++11 constructor overload resolution and initialiser_lists: clang++ and g++ disagreeC++11构造函数重载解析和initialiser_lists:clang++和g++不一致
【发布时间】:2013-07-16 07:09:01
【问题描述】:

我有一小段 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:

$ 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++ 对此相当严格),但我还不够标准律师能够解开应该是什么这里的正确行为!

【问题讨论】:

  • 函数“f”中两个名为“b2b”的局部变量只是一个错字,不是问题的原因,我认为......
  • 在我看来,从 initializer_list 创建 A 和从 T 创建 P 都应该是完全匹配的,因此会产生歧义。我无法解释的是为什么编译器会选择不同的来抱怨。
  • 是的,我很高兴有一个(或两个!)选项模棱两可,但编译器不同意它应该是什么,我不知道哪个应该是正确的自己。因此要求澄清。

标签: c++ templates c++11 ambiguous constructor-overloading


【解决方案1】:

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

B1 b1a(x, {y});

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

可以调用带有const A&lt;Y&gt;&amp;参数的构造函数——{y}将作为A&lt;Y&gt;构造函数的参数,并将初始化该构造函数的std::initializer_list&lt;Y&gt;

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

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

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

对于参数类型为const A&lt;Y&gt;&amp;的构造函数,稍微复杂一些。重载解析中的转换成本规则计算初始化std::initializer_list&lt;T&gt; 的成本要求花括号列表的每个元素都可转换为T。但是我们之前说过{y} 不能转换为Y(因为它是一个聚合)。现在重要的是要知道std::initializer_list&lt;T&gt; 是否是一个聚合。坦率地说,我不知道是否必须根据标准库条款将其视为聚合。

如果我们认为它是非聚合的,那么我们将考虑 std::initializer_list&lt;Y&gt; 的复制构造函数,然而这又会触发完全相同的测试序列(导致重载决议检查中的“无限递归”) .由于这是相当奇怪且不可实现的,我认为任何实现都不会走这条路。

如果我们将std::initializer_list 作为一个聚合,我们会说“不,没有找到转换”(参见上面的聚合问题)。在这种情况下,由于我们不能用单个初始化列表作为一个整体来调用初始化构造函数,{{y}} 将被拆分为多个参数,A&lt;Y&gt; 的构造函数将分别获取每个参数。因此,在这种情况下,我们最终会得到 {y}std::initializer_list&lt;Y&gt; 初始化为单个参数 - 这非常好,并且像一个魅力一样工作。

所以std::initializer_list&lt;T&gt; 是一个聚合的假设下,这很好,并且成功地调用了第二个构造函数

B2 b2a(x, {P<Y>(y)});

在这种情况下和下一种情况下,我们不再有上面Y 的聚合问题,因为P&lt;Y&gt; 有一个用户提供的构造函数。

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

对于A&lt;P&lt;Y&gt;&gt;参数构造函数,和上面A&lt;Y&gt;{y}初始化的情况一样:由于std::initializer_list&lt;P&lt;Y&gt;&gt;可以被{P&lt;Y&gt; object}初始化,所以参数列表不会被拆分,因此大括号用于初始化构造函数的std::initializer_list&lt;T&gt;

现在,两个构造函数都可以正常工作。它们在这里就像重载函数一样,在这两种情况下它们的第二个参数都需要用户定义的转换。只有在两种情况下都使用相同的转换函数或构造函数时,才能比较用户定义的转换序列——这里不是这种情况。因此,这在 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}-&gt;X 成为身份转换来实现的(就像X-&gt;x 一样)。目前为用户自定义转换。

所以,这里有歧义

B2 b2b(x, {{P<Y>(y)}});

对于带有参数const P&lt;Y&gt;&amp; 的构造函数,我们再次拆分参数并将{P&lt;Y&gt; object} 参数传递给P&lt;Y&gt; 的构造函数。请记住,P&lt;Y&gt; 有一个复制构造函数。但这里的复杂之处在于我们不允许使用它(参见 13.3.3.1p4),因为它需要用户定义的转换。剩下的唯一构造函数是采用Y 的构造函数,但Y 不能由{P&lt;Y&gt; object} 初始化。

对于带有参数A&lt;P&lt;Y&gt;&gt; 的构造函数,{{P&lt;Y&gt; object}} 可以初始化std::initializer_list&lt;P&lt;Y&gt;&gt;,因为{P&lt;Y&gt; object} 可以转换为P&lt;Y&gt;(除了上面的Y - dang,聚合)。

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


4 个总结

  • 第二个构造函数调用成功
  • 假设std::initializer_list&lt;T&gt;是一个聚合,这样就可以了,调用第二个构造函数成功
  • 这里有歧义
  • 第二个构造函数调用成功

【讨论】:

  • 谢谢,看来我的困惑并非完全没有根据 :)
猜你喜欢
  • 2016-03-18
  • 2011-04-03
  • 2010-12-16
  • 1970-01-01
  • 2019-10-24
  • 2013-12-24
  • 1970-01-01
  • 1970-01-01
  • 2015-07-02
相关资源
最近更新 更多