多重继承 operator() 的重载解析

Overload resolution for multiply inherited operator()

首先,考虑这个 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 在每个基 class 中被单独考虑以进行重载解析,因此调用是不明确的。在 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声明中的一个,而不取消另一个,那么三个编译器都会编译失败,因为它们只会考虑[=16=带来的函数] 在重载解析期间,因此其中一个调用将由于类型不匹配而失败。记住这一点;我们稍后再谈。

现在考虑以下代码:

#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, or if both are uncommented. But if you only comment out one but not the other, things are suddenly different again 的情况也是如此:现在只有 MSVC 像我预期的那样抱怨,而 Clang 和 gcc 都认为这很好 - 并使用两个继承的成员进行重载解析,尽管只有一个被引入using!

问题 #2:在这种情况下谁是正确的?同样,我希望它是 MSVC,但为什么 Clang 和 gcc 都不同意呢?而且,更重要的是,为什么这与之前的片段不同?我希望 lambda 类型的行为与重载 operator()...

的手动定义类型完全相同

class class 中的名称查找规则 class C 仅当 C 本身不直接包含名称 [=20] 时才会发生=]:

The following steps define the result of merging lookup set S(f,Bi) into the intermediate S(f,C):

  • If each of the subobject members of S(f,Bi) is a base class subobject of at least one of the subobject members of S(f,C), or if S(f,Bi) is empty, S(f,C) is unchanged and the merge is complete. Conversely, if each of the subobject members of S(f,C) is a base class subobject of at least one of the subobject members of S(f,Bi), or if S(f,C) is empty, the new S(f,C) is a copy of S(f,Bi).

  • Otherwise, if the declaration sets of S(f,Bi) and S(f,C) differ, the merge is ambiguous: the new S(f,C) is a lookup set with an invalid declaration set and the union of the subobject sets. In subsequent merges, an invalid declaration set is considered different from any other.

  • Otherwise, the new S(f,C) is a lookup set with the shared set of declarations and the union of the subobject sets.

如果我们有两个基 classes,每个都声明相同的名称,派生的 class 没有使用 using 声明引入,在派生的 class 会 运行 与第二个要点冲突,查找应该会失败。在这方面,您所有的示例都基本相同。

Question #1: who is correct in this case?

gcc 是正确的。 printoperator() 之间的唯一区别是我们要查找的名称。

Question #2: who is correct in this case?

这是与 #1 相同的问题 - 除了我们有 lambdas(它给你未命名的 class 重载 operator() 类型)而不是显式 class 类型。出于同样的原因,代码应该是错误格式的。至少对于 gcc,这是 bug 58820.

您对第一个代码的分析不正确。没有重载决议。

名称查找 过程完全发生在重载解析之前。名称查找确定 id-expression 解析到哪个范围。

如果通过名称查找规则找到唯一范围,然后重载解析开始:该范围内该名称的所有实例形成重载集。

但在您的代码中,名称查找失败。该名称未在 foo 中声明,因此搜索基 classes。如果在多个直接基数中找到该名称 class,则程序格式错误,错误消息将其描述为一个不明确的名称。


名称查找规则没有针对重载运算符的特殊情况。您应该会发现代码:

f.operator()(123);

失败的原因与 f.print 失败的原因相同。但是,您的第二个代码中还有另一个问题。 f(123) 未定义为始终表示 f.operator()(123);。事实上 C++14 中的定义是 [over.call]:

operator() shall be a non-static member function with an arbitrary number of parameters. It can have default arguments. It implements the function call syntax

postfix-expression ( expression-list opt )

where the postfix-expression evaluates to a class object and the possibly empty expression-list matches the parameter list of an operator() member function of the class. Thus, a call x(arg1,...) is interpreted as x.operator()(arg1, ...) for a class object x of type T if T::operator()(T1, T2, T3) exists and if the operator is selected as the best match function by the overload resolution mechanism (13.3.3).

这实际上对我来说似乎是一个不精确的规范,所以我可以理解不同的编译器会产生不同的结果。什么是T1、T2、T3?这是否意味着参数的类型? (我怀疑不是)。当存在多个 operator() 函数且只接受一个参数时,T1、T2、T3 是什么?

“如果 T::operator() 存在”到底是什么意思?它可能意味着以下任何一种:

  1. operator()T 中声明。
  2. T 范围内 operator() 的非限定查找成功,并且使用给定参数对该查找集执行重载解析成功。
  3. 调用上下文中 T::operator() 的合格查找成功,并且使用给定参数对该查找集执行重载解析成功。
  4. 还有别的吗?

从这里开始(无论如何对我来说)我想理解为什么标准不简单地说 f(123) 意味着 f.operator()(123);,当且仅当后者格式错误。实际措辞背后的动机可能会揭示意图,从而揭示哪个编译器的行为符合意图。

巴里答对了第一名。你的 #2 遇到了一个极端情况:无捕获的非泛型 lambda 有一个隐式转换为函数指针,它在不匹配的情况下被使用。也就是说,给定

struct foo : foo_int, foo_str {
    using foo_int::operator();
    //using foo_str::operator();
    foo(): foo_int(print_int), foo_str(print_str) {}
} f;

using fptr_str = void(*)(const char*);

f("hello") 等同于 f.operator fptr_str()("hello"),将 foo 转换为指向函数的指针并调用它。如果您在 -O0 处编译,您实际上可以在程序集被优化之前看到对转换函数的调用。将 init-capture 放入 print_str 中,由于隐式转换消失,您将看到一个错误。

有关更多信息,请参阅 [over.call.object]