【问题标题】:Strange case of C++11 overload resolutionC++11 重载解析的奇怪案例
【发布时间】:2011-10-24 04:01:15
【问题描述】:

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

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 的有效初始化程序,1S 的初始化程序(它可以从 int 隐式构造,因为第二个其构造函数的参数有一个默认值),而 2 是一个可变参数......但我不明白为什么这会是一个更好的匹配,特别是看到有问题的复制构造函数被删除了。

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

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

编辑 2:我进一步简化了 sn-p 以获得更令人不安的故障:

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 也给出了同样的错误(减去constexprs 和它不会隐式生成的移动构造函数)。

我发现非常很难相信这是语言设计者的意图......

【问题讨论】:

  • 编译器不考虑“我可以这样称呼吗?”直到它决定“我应该叫什么”之后。因此,不幸的是,被删除的复制构造函数根本与主题无关..
  • @Dennis:SFINAE 不是反例吗?
  • @bdonlan:这样想:如果我写了C c(C{1, 2}),它将别无选择,只能调用复制构造函数。如果我写了C c(S{1, 2}),它将别无选择,只能调用第一个构造函数。但是我写了C c({1, 2}),那么它会调用哪个呢?在常识层面上,它不应该尝试调用已删除的函数是理所当然的......但是编译器在常识上从来都不是强大的,是吗? =P
  • 我觉得有某种括号/括号可以解决这个问题。
  • @HighCommander4:调用和模板实例化是不同的——在这种情况下,甚至没有尝试模板实例化

标签: c++ c++11 overload-resolution


【解决方案1】:

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

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

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

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

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

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

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

【讨论】:

  • 我不遵循这部分:“标准明确禁止用户定义的移动或复制构造函数参数的转换可用于从 {...} 初始化类 C 对象,因为这可能会导致不必要的歧义”——这是否意味着编译器不应该将复制构造函数视为候选对象,因为它需要用户定义的转换?
  • @High 请再次检查。我在上面有一个想法。
  • @High 否,因为您没有通过{ ... } 初始化 C 类对象。你用( ... )初始化它。如果您愿意C c{1};,那么我所说的适用。但是您确实在大括号周围放置了括号,这导致它不再是列表初始化,而是在直接初始化期间对一组函数(在本例中为构造函数)的正常调用。
  • 这是我不同意的部分:“从 {1, 2} 构造 C 也很简单,并采用你的构造函数模板” - 它如何像 S{1 一样简单, 2},当 C{1, 2} 需要从 1 到 S 的额外转换?
  • 考虑struct B; struct A { operator int(); operator B(); }; struct B { B(int); }; A a; B b{a};b 的声明格式正确,不使用 B(B&amp;&amp;) 构造函数,因为它需要用户定义的从 AB 的转换。如果你说B b(a);B b({a});,代码就会变得模棱两可。
【解决方案2】:

您对为什么它可以从该初始值设定项列表创建 C 的解释可能是正确的。 ideone 愉快地编译了你的示例代码,两个编译器都不对。假设创建副本是有效的,但是......

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

SFINAE 需要不同类型的错误...它们发生在第一步,检查哪些函数可能匹配时。如果简单地创建函数导致错误,则忽略该错误,并且该函数不被视为可能的重载。在枚举了可能的重载之后,此错误抑制功能将被关闭,您将无法获得所获得的结果。

【讨论】:

  • 它是模板化的 C 构造函数。为什么它更喜欢模板函数而不是非模板函数?
  • S{1, 2} 的构造中,1, 2 与参数列表完美匹配,而对于 C{1, 2} 的构造,从 @987654328 的隐式转换@ 到 S 对象是必需的吗?我认为这会使S{1, 2} 成为更好的重载。
  • @Chris:Dennis 表示非模板化 C 构造函数(复制构造函数)优于模板化 C 构造函数。
  • @High:坦率地说,我开始得出结论,即使考虑复制构造函数,您的编译器也存在问题,因为它需要按顺序进行两个用户定义的转换 [从 1->S 和从 {S , 2}->​​C]。此外,这不是复制初始化的示例,因此不应该首先考虑复制构造函数。
  • 但是,如果它是有效的,那么两者都需要隐式转换。来自 {S, 2}->​​C 或来自 {1, 2}->​​S,使两者同样可行。由于其中一个不是模板函数,因此选择了那个。
猜你喜欢
  • 1970-01-01
  • 2011-04-27
  • 1970-01-01
  • 2012-07-02
  • 2014-03-15
  • 2015-11-05
  • 2016-01-03
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多