9

我今天遇到了一个相当奇怪的重载解决案例。我将其简化为以下内容:

struct S
{
    S(int, int = 0);
};

class C
{
public:
    template <typename... Args>
    C(S, Args... args);

    C(const C&) = delete;
};

int main()
{
    C c({1, 2});
}

我完全期望C c({1, 2})匹配 的第一个构造函数C,可变参数的数量为零,{1, 2}并被视为S对象的初始化列表构造。

但是,我收到一个编译器错误,表明它与已删除的 C 复制构造函数匹配!

test.cpp: In function 'int main()':
test.cpp:17:15: error: use of deleted function 'C(const C &)'
test.cpp:12:5: error: declared here

我可以看到它是如何工作的 -{1, 2}可以解释为 C 的有效初始化程序,1作为初始化程序S(它可以从 int 隐式构造,因为它的构造函数的第二个参数具有默认值),并且2作为一个可变参数......但我不明白为什么这会是一个更好的匹配,特别是看到有问题的复制构造函数被删除。

有人可以解释一下这里起作用的重载解决规则,并说明是否有一种解决方法不涉及在构造函数调用中提及 S 的名称?

编辑:由于有人提到该片段使用不同的编译器编译,我应该澄清一下,我在 GCC 4.6.1 中遇到了上述错误。

编辑 2:我进一步简化了代码片段,以得到更令人不安的失败:

struct S
{
    S(int, int = 0);
};

struct C
{
    C(S);
};

int main()
{
    C c({1});
}

错误:

test.cpp: In function 'int main()':
test.cpp:13:12: error: call of overloaded 'C(<brace-enclosed initializer list>)' is ambiguous
test.cpp:13:12: note: candidates are:
test.cpp:8:5: note: C::C(S)
test.cpp:6:8: note: constexpr C::C(const C&)
test.cpp:6:8: note: constexpr C::C(C&&)

而这一次,GCC 4.5.1 也给出了同样的错误(减去constexpr它没有隐式生成的 s 和 move 构造函数)。

很难相信这是语言设计者的意图......

4

2 回答 2

6

因为C c({1, 2});您有两个可以使用的构造函数。因此发生重载决议并查看要采取的功能

C(S, Args...)
C(const C&)

Args如您所见,将被扣除为零。因此,编译器将构造S与构造C临时 out 进行比较{1, 2}。构造Sfrom{1, 2}是直截了当的,并采用您声明的构造函数S. C从also构造{1, 2}是直截了当的,并采用您的构造函数模板(复制构造函数不可行,因为它只有一个参数,但传递了两个参数 -1和- )。2这两种转换顺序没有可比性。因此,如果不是因为第一个是模板这一事实,这两个构造函数将是模棱两可的。因此 GCC 将更喜欢非模板,选择已删除的复制构造函数并为您提供诊断。

现在对于您的C c({1});测试用例,可以使用三个构造函数

C(S)
C(C const&)
C(C &&)

对于最后两个,编译器会更喜欢第三个,因为它将右值绑定到右值。但是,如果您考虑C(S)反对,C(C&&)您将不会在两种参数类型之间找到赢家,因为您可以从 aC(S)构造一个,并且您可以通过使用构造函数从 a初始化一个临时值(标准明确禁止用户定义的参数转换可用于从 初始化类对象的移动或复制构造函数,因为这可能会导致不必要的歧义;这就是为什么这里不考虑to的转换,而只考虑从to的转换S{1}C(C&&)C{1}C(S)C{...}1C&&1S)。但是这一次,与您的第一个测试用例相反,构造函数都不是模板,因此您最终会产生歧义。

这完全是事情的预期工作方式。C++ 中的初始化很奇怪,所以很难让每个人都“直观”地了解所有内容。即使是上面的简单示例也会很快变得复杂。当我写下这个答案时,一个小时后我偶然再次查看它,我注意到我忽略了一些东西,不得不修复答案。

于 2011-10-24T20:38:15.347 回答
4

您对为什么它可以C从该初始化程序列表创建一个的解释可能是正确的。 ideone 很高兴地编译您的示例代码,并且两个编译器都不正确。假设创建副本是有效的,但是......

所以从编译器的角度来看,它有两种选择:创建一个新的S{1,2}并使用模板构造函数,或者创建一个新的C{1,2}并使用复制构造函数。通常,非模板函数优于模板函数,因此选择了复制构造函数。 然后它查看是否可以调用该函数......它不能,所以它会吐出一个错误。

SFINAE 需要不同类型的错误......它们发生在第一步,检查哪些函数可能匹配时。如果简单地创建函数导致错误,则忽略该错误,并且该函数不被视为可能的重载。在枚举了可能的重载之后,这个错误抑制被关闭,你就会被你得到的东西困住。

于 2011-10-24T04:22:58.430 回答