C++11 重载解析的奇怪情况
今天我遇到了一个相当奇怪的重载解析案例。我将其简化为以下内容:
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 和它不会隐式生成的移动构造函数)。
我发现非常很难相信这就是语言设计者的意图......
I came across a rather strange case of overload resolution today. I reduced it to the following:
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});
}
I fully expected C c({1, 2})
to match the first constructor of C
, with the number of variadic arguments being zero, and {1, 2}
being treated as an initializer list construction of an S
object.
However, I get a compiler error that indicates that instead, it matches the deleted copy constructor of 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
I can sort of see how that might work - {1, 2}
can be construed as a valid initializer for C, with the 1
being an initializer for the S
(which is implicitly constructible from an int because the second argument of its constructor has a default value), and the 2
being a variadic argument... but I don't see why that would be a better match, especially seeing as the copy constructor in question is deleted.
Could someone please explain the overload resolution rules that are in play here, and say whether there is a workaround that does not involve mentioning the name of S in the constructor call?
EDIT: Since someone mentioned the snippet compiles with a different compiler, I should clarify that I got the above error with GCC 4.6.1.
EDIT 2: I simplified the snippet even further to get an even more disturbing failure:
struct S
{
S(int, int = 0);
};
struct C
{
C(S);
};
int main()
{
C c({1});
}
Errors:
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&&)
And this time, GCC 4.5.1 gives the same error, too (minus the constexpr
s and the move constructor which it does not generate implicitly).
I find it very hard to believe that this is what the language designers intended...
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(2)
对于
C c({1, 2});
您有两个可以使用的构造函数。因此,会进行重载解析,并查看采用Args
的函数将被推导为零,正如您所计算的那样。因此,编译器会将构造S
与从{1, 2}
构造C
临时对象进行比较。从{1, 2}
构造S
非常简单,并采用您声明的S
构造函数。从{1, 2}
构造C
也很简单,并采用构造函数模板(复制构造函数不可行,因为它只有一个参数,但有两个参数 - < code>1 和2
- 已通过)。这两个转换顺序没有可比性。因此,如果不是因为第一个构造函数是模板,那么这两个构造函数将是不明确的。因此,GCC 会更喜欢非模板,选择已删除的复制构造函数,并为您提供诊断信息。现在,对于您的
C c({1});
测试用例,可以使用三个构造函数。对于最后两个构造函数,编译器会更喜欢第三个构造函数,因为它将右值绑定到右值。但是,如果您考虑使用
C(S)
与C(C&&)
,您将无法在这两种参数类型之间找到获胜者,因为对于C(S )
您可以从{1}
构造S
,对于C(C&&)
您可以初始化 < code>C 临时来自{1}
通过采用C(S)
构造函数(标准明确禁止用户定义的移动或复制构造函数的参数转换可用于初始化来自{...}
的类C
对象,因为这可能会导致不必要的歧义;这就是将1
转换为的原因;这里不考虑C&&
,只考虑转换从1
到S
)。但这一次,与您的第一个测试用例相反,两个构造函数都不是模板,因此您最终会产生歧义。这完全就是事情的运作方式。 C++ 中的初始化很奇怪,因此遗憾的是,让每个人都“直观”地了解所有内容是不可能的。即使是上面的简单示例也会很快变得复杂。当我写下这个答案时,一个小时后,我偶然再次查看它,我发现我忽略了一些东西,必须修正答案。
For
C c({1, 2});
you have two constructors that can be used. So overload resolution takes place and looks what function to takeArgs
will have been deduced to zero as you figured out. So the compiler compares constructingS
against constructing aC
temporary out of{1, 2}
. ConstructingS
from{1, 2}
is straight forward and takes your declared constructor ofS
. ConstructingC
from{1, 2}
also is straight forward and takes your constructor template (the copy constructor is not viable because it has only one parameter, but two arguments -1
and2
- are passed). These two conversion sequences are not comparable. So the two constructors would be ambiguous, if it weren't for the fact that the first is a template. So GCC will prefer the non-template, selecting the deleted copy constructor and will give you a diagnostic.Now for your
C c({1});
testcase, three constructors can be usedFor the two last, the compiler will prefer the third because it binds an rvalue to an rvalue. But if you consider
C(S)
againstC(C&&)
you won't find a winner between the two parameter types because forC(S)
you can construct anS
from a{1}
and forC(C&&)
you can initialize aC
temporary from a{1}
by taking theC(S)
constructor (the Standard explicitly forbids user defined conversions for a parameter of a move or copy constructor to be usable for an initialization of a classC
object from{...}
, since this could result in unwanted ambiguities; this is why the conversion of1
toC&&
is not considered here but only the conversion from1
toS
). But this time, as opposed to your first testcase, neither constructor is a template so you end up with an ambiguity.This is entirely how things are intended to work. Initialization in C++ is weird so getting everything "intuitive" to everyone is going to be impossible sadly. Even a simple example as above quickly gets complicated. When I wrote this answer and after an hour I looked at it again by accident I noticed I overlooked something and had to fix the answer.
您对为什么它可以从该初始值设定项列表创建
C
的解释可能是正确的。 ideone 愉快地编译您的示例代码,并且两个编译器都不可能都是正确的。然而,假设创建副本是有效的...因此从编译器的角度来看,它有两种选择:创建一个新的
S{1,2}
并使用模板化构造函数,或者创建一个新的C{1,2}
并使用复制构造函数。通常,非模板函数优于模板函数,因此选择复制构造函数。 然后它会检查该函数是否可以被调用...它不能,因此它会抛出一个错误。SFINAE 需要不同类型的错误......它们发生在第一步,检查哪些函数可能匹配时。如果简单地创建函数会导致错误,则该错误将被忽略,并且该函数不会被视为可能的重载。枚举可能的重载后,此错误抑制功能将被关闭,您将只能得到所得到的结果。
You might be correct in your interpretation of why it can create a
C
from that initializer list. ideone happily compiles your example code, and both compilers can't be correct. Assuming creating the copy is valid, however...So from the compiler's point of view, it has two choices: Create a new
S{1,2}
and use the templated constructor, or create a newC{1,2}
and use the copy constructor. As a rule, non-template functions are preferred over template ones, so the copy constructor is chosen. Then it looks at whether or not the function can be called... it can't, so it spits out an error.SFINAE requires a different type of error... they occur during the first step, when checking to see which functions are possible matches. If simply creating the function results in an error, that error is ignored, and the function not considered as a possible overload. After the possible overloads are enumerated, this error suppression is turned off and you're stuck with what you get.