【问题标题】:Overload resolution for multiply inherited operator()多重继承运算符()的重载解决方案
【发布时间】:2017-06-08 07:34:27
【问题描述】:

首先,考虑这段 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

现在考虑下面的sn-p,唯一的区别是我们使用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 类型中获取它。同样,您希望结果与之前的 sn-p 一致;这适用于both using declarations are commented outboth are uncommented 的情况。但是,如果您只注释掉一个而不注释另一个,那么事情就是suddenly different again:现在只有 MSVC 会像我期望的那样抱怨,而 Clang 和 gcc 都认为这很好 - 并且使用两个继承的成员来解决重载问题,尽管只有一个被using带进来!

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

【问题讨论】:

    标签: c++ lambda language-lawyer multiple-inheritance overload-resolution


    【解决方案1】:

    巴里的第一名是对的。您的 #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 编译,您实际上可以在程序集中看到对转换函数的调用,然后再优化它。在print_str 中放置一个init-capture,你会看到一个错误,因为隐式转换消失了。

    欲了解更多信息,请参阅[over.call.object]

    【讨论】:

    • 哇,我以前从未见过这个。很有趣。
    • 啊,太好了!所以 gcc 是唯一一个在这里始终正确的。不是我所期望的。
    • 虽然我仍然不清楚为什么将using 用于other operator() 会有所不同。我明白为什么这个using 使 int 调用合法;但为什么它会影响字符串文字调用呢?
    • 或者,或者,当根本没有usings 时,为什么不执行相同的逻辑?它不应该只是从两个继承的转换运算符构建候选集,然后根据参数类型明确地解决这两个函数指针转换吗?
    • @PavelMinaev 这两个转换函数返回不同的类型,因此具有不同的名称。所以他们从不模棱两可。什么是模棱两可的 - 如果你没有 using - 是函数调用运算符的名称查找。
    【解决方案2】:

    仅当C 本身不直接包含名称[class.member.lookup]/6 时,才会在类C 的基类中查找名称:

    以下步骤定义了合并查找集S(f,Bi)的结果 进入中间S(f,C)

    • 如果 S(f,Bi) 的每个子对象成员都是 S(f,C) 的至少一个子对象成员的基类子对象,或者如果 S(f,Bi) 为空, S(f,C) 不变,合并完成。相反,如果 S(f,C) 的每个子对象成员都是 S(f,Bi) 的至少一个子对象成员的基类子对象,或者如果 S(f,C) 为空,则新的 S (f,C) 是 S(f,Bi) 的副本。

    • 否则,如果 S(f,Bi) 和 S(f,C) 的声明集不同,则合并不明确:新的 S(f,C) 是具有无效声明集和子对象集并集的查找集。在随后的合并中,无效的声明集被认为与其他任何不同。

    • 否则,新的 S(f,C) 是具有共享声明集和子对象集并集的查找集。

    如果我们有两个基类,每个都声明相同的名称,派生类没有使用 using 声明,那么在派生类中查找该名称将与第二个要点和查找发生冲突应该失败。在这方面,您的所有示例都基本相同。

    问题 #1:在这种情况下谁是正确的?

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

    问题 #2:在这种情况下谁是正确的?

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

    【讨论】:

    • 请注意,对于第二个问题,类似于您链接到的错误中的情况 - 没有“使用” - 被 gcc 拒绝(所以看起来该错误已修复?) .这里有一个不同的错误,其中仅为其中一个基指定“使用”足以使运算符来自重载解决集的两个部分。
    • 最后一种情况是 MSVC 错误(对于仅注释一出的情况)。名称查找是明确的,对函数指针的隐式转换用于与命名 operator() 不匹配的调用。
    • 嗯,58820 是一个虚假的投诉,即 GCC 报告了歧义,而实际上存在歧义。它应该被关闭为无效。
    【解决方案3】:

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

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

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

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


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

    f.operator()(123);
    

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

    operator() 应该是具有任意数量参数的非静态成员函数。它可以有默认参数。它实现了函数调用语法

    后缀表达式(表达式列表选择)

    其中后缀表达式计算为类对象,并且可能为空的表达式列表与该类的operator() 成员函数的参数列表匹配。因此,如果 T::operator()(T1, T2, T3) 存在并且运算符被重载决议机制 (13.3.3) 选择为最佳匹配函数,则调用 x(arg1,...) 被解释为类型 T 的类对象 x 的 x.operator()(arg1, ...)

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

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

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

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

    【讨论】:

    • 它没有说f(123) 表示f.operator()(123) 的原因是允许将表达式f 隐式转换为指向函数类型的指针。请参阅@T.C. 的回答。
    • @aschepler 好的。我仍然不清楚“如果 T::operator()(T1, T2, T3) 存在”是什么意思(以及这样的运算符是否存在于 OP 的代码中)
    • 是的,这是一个非常糟糕的段落。
    • @aschepler 我在forum 上发布了关于它的信息,但回复基本上是耸耸肩?
    • @Barry 只需提交编辑问题,(最终)可能会有人查看它。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2019-04-09
    • 1970-01-01
    • 1970-01-01
    • 2017-12-30
    • 1970-01-01
    • 2012-11-15
    • 2011-12-05
    相关资源
    最近更新 更多