首先,考虑这个C代码:
#include <stdio.h>
struct foo_int {
void print(int x) {
printf("int %d\n", x);
}
};
struct foo_str {
void print(const char* x) {
printf("str %s\n", x);
}
};
struct foo : foo_int, foo_str {
//using foo_int::print;
//using foo_str::print;
};
int main() {
foo f;
f.print(123);
f.print("abc");
}
正如根据标准所预期的那样,这无法编译,因为 print
在每个基类中被单独考虑用于重载解析,因此调用是不明确的 . 这是Clang(4.0),gcc(6.3)和MSVC(17.0)的情况 - 参见godbolt结果here .
现在考虑以下片段,唯一的区别是我们使用 operator()
而不是 print
:
#include <stdio.h>
struct foo_int {
void operator() (int x) {
printf("int %d\n", x);
}
};
struct foo_str {
void operator() (const char* x) {
printf("str %s\n", x);
}
};
struct foo : foo_int, foo_str {
//using foo_int::operator();
//using foo_str::operator();
};
int main() {
foo f;
f(123);
f("abc");
}
我希望结果与之前的情况相同,但it is not the case - 而gcc仍然抱怨,Clang和MSVC可以编译这个罚款!
问题1:在这种情况下谁是正确的?我希望它是gcc,但事实上其他两个不相关的编译器在这里给出了一致的不同结果让我想知道我是否遗漏了标准中的某些内容,并且当操作符未使用函数语法调用时,它们会有所不同 .
另请注意,如果您只取消注释其中一个 using
声明而不取消另一个声明,那么所有三个编译器都将无法编译,因为它们只会在重载解析期间考虑 using
带来的函数,因此其中一个调用将失败由于类型不匹配 . 记住这一点;我们稍后再回过头来看看 .
现在考虑以下代码:
#include <stdio.h>
auto print_int = [](int x) {
printf("int %d\n", x);
};
typedef decltype(print_int) foo_int;
auto print_str = [](const char* x) {
printf("str %s\n", x);
};
typedef decltype(print_str) foo_str;
struct foo : foo_int, foo_str {
//using foo_int::operator();
//using foo_str::operator();
foo(): foo_int(print_int), foo_str(print_str) {}
};
int main() {
foo f;
f(123);
f("foo");
}
同样,和以前一样,除了现在我们没有显式定义 operator()
,而是从lambda类型获取它 . 同样,您希望结果与之前的代码段保持一致;对于both using declarations are commented out或both are uncommented的情况也是如此 . 但是如果你只注释掉一个而不是另一个,那么事情就是suddenly different again:现在只有MSVC会按照我的预期抱怨,而Clang和gcc都认为这很好 - 并且使用两个继承成员进行重载解析,尽管只有一个被带来在 using
!
问题2:在这种情况下谁是正确的?再一次,我希望它是MSVC,但那么为什么Clang和gcc都不同意?而且,更重要的是,为什么这与前面的代码片段不同?我希望lambda类型的行为与手动定义的类型完全相同,重载 operator()
...
3 回答
巴里排名第一 . 你的#2命中了一个角落:无捕获的非泛型lambdas有一个隐式转换为函数指针,它在不匹配的情况下使用 . 也就是说,给定
f("hello")
相当于f.operator fptr_str()("hello")
,将foo
转换为指向函数的指针并调用它 . 如果在-O0
进行编译,实际上可以在程序集优化之前看到对程序集中转换函数的调用 . 在print_str
中放置一个init-capture,你会看到一个错误,因为隐式转换消失了 .有关更多信息,请参阅[over.call.object] .
仅当
C
本身不直接包含名称[class.member.lookup]/6时,才会在类C
的基类中进行名称查找规则:如果我们有两个基类,每个都声明了相同的名称,派生类没有引入using声明,那么在派生类中查找该名称将与第二个项目符号点相冲突,并且查找应该失败 . 在这方面,您的所有示例基本相同 .
gcc是对的 .
print
和operator()
之间的唯一区别是我们正在查找的名称 .这是与#1相同的问题 - 除了我们有lambdas(它给你带有重载
operator()
的未命名类类型)而不是显式类类型 . 代码因同样的原因应该是格式不正确的 . 至少对于gcc来说,这是bug 58820 .您对第一个代码的分析不正确 . 没有重载解决方案 .
名称查找过程完全在重载解析之前发生 . 名称查找确定要解析id-expression的范围 .
如果通过名称查找规则找到唯一范围,则开始重载解析:该范围内该名称的所有实例形成重载集 .
但在您的代码中,名称查找失败 . 该名称未在
foo
中声明,因此将搜索基类 . 如果在多个直接基类中找到该名称,则该程序格式错误,并且错误消息将其描述为不明确的名称 .名称查找规则没有重载运算符的特殊情况 . 你应该找到代码:
失败的原因与
f.print
失败的原因相同 . 但是,第二个代码中还有另一个问题 .f(123)
未定义为始终含义f.operator()(123);
. 事实上,C 14中的定义是[over.call]:这实际上对我来说似乎是一个不精确的规范,所以我可以理解不同的编译器会出现不同的结果 . 什么是T1,T2,T3?这是否意味着参数的类型? (我怀疑不是) . 什么是T1,T2,T3多个
operator()
函数存在时,只接受一个参数?无论如何,“if
T::operator()
存在”是什么意思?它可能意味着以下任何一种情况:operator()
在T
中声明 .T
范围内的operator()
的非限定查找成功并对具有给定参数的查找集执行重载解析成功 .在调用上下文中对
T::operator()
的限定查找成功,并对该查找集执行重载解析,并且给定的参数成功 .别的什么?
从这里开始(无论如何),我想理解为什么标准并不是简单地说
f(123)
意味着f(123)
,前者是不正确的,当且仅当后者形成不良时 . 实际措辞背后的动机可能会揭示意图,因此编译器的行为与意图相匹配 .