当前位置: 首页 > 知识库问答 >
问题:

多重继承运算符()的重载决策

宋烨烁
2023-03-14

首先,考虑这个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");
}

根据标准的预期,这将无法编译,因为< code>print在每个基类中都被单独考虑,以便进行重载解析,因此调用是不明确的。这是Clang (4.0)、gcc (6.3)和MSVC (17.0)的情况——见godbolt结果。

现在考虑以下片段,其唯一的区别是我们使用oper()而不是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");
}

我希望结果与前一种情况相同,但事实并非如此 - 虽然gcc仍然抱怨,但Clang和MSVC可以编译这个罚款!

问题1:在这种情况下,谁是正确的?我希望它是gcc,但事实上,另外两个无关的编译器在这里给出的结果总是不同的,这让我怀疑我是否在标准中遗漏了一些东西,而当运算符不使用函数语法调用时,情况就不同了。

还要注意,如果只取消注释其中一个< code>using声明,而不取消注释另一个声明,那么所有三个编译器都将无法编译,因为它们在重载解析期间只考虑< code>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类型获取它。同样,您希望结果与前面的代码片段一致;对于使用的声明都被注释掉,或者两者都被取消注释的情况,情况也是如此。但是如果你只注释其中一个,而不注释另一个,情况又会突然不同:现在只有MSVC会像我预料的那样抱怨,而Clang和gcc都认为这很好,并且使用两个继承的成员进行重载解析,尽管只有一个成员是由using引入的!

问题2:在这种情况下,谁是正确的?再一次,我希望它是MSVC,但为什么克朗和gcc都不同意呢?而且,更重要的是,为什么这与前面的代码段不同?我希望 lambda 类型的行为与具有重载运算符()的手动定义类型完全相同...


共有3个答案

胡景澄
2023-03-14
匿名用户

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

名称查找过程完全发生在重载解析之前。名称查找确定id表达式解析到的范围。

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

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

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

f.operator()(123);

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

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

后缀表达式(表达式列表opt)

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

对我来说,这实际上似乎是一个不精确的规范,所以我可以理解不同的编译器得出不同的结果。什么是T1、T2、T3?它是指参数的类型吗?(我怀疑不是)。当存在多个运算符()函数,只接受一个参数时,什么是T1、T2、T3?

“如果 T::运算符() 存在”是什么意思呢?它可能意味着以下任何一种情况:

    < li> 运算符()在< code>T中声明。 < li >在< code>T范围内对< code>operator()的非限定查找成功,并且使用给定参数对该查找集执行重载决策成功。 < li >调用上下文中< code>T::operator()的限定查找成功,并且使用给定参数对该查找集执行重载决策成功。 < li >还有别的吗?

从这里开始(无论如何对我来说),我想理解为什么标准没有简单地说f(123)意味着f。运算符()(123),当且仅当后者为病态时,前者为病态。实际措辞背后的动机可能会揭示意图,因此哪个编译器的行为与意图相匹配。

王季萌
2023-03-14

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

以下步骤定义了将查找集 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是正确的。printoper()之间的唯一区别是我们正在查找的名称。

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

    这与#1的问题相同——只是我们有lambdas(它为您提供带有重载运算符()的未命名类类型)而不是显式类类型。出于同样的原因,代码应该格式错误。至少对于gcc来说,这是错误58820。

  • 许兴文
    2023-03-14

    巴里得到了#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,您将看到一个错误,因为隐式转换会消失。

    如需更多信息,请参阅[over.call.object]。

     类似资料:
    • 主要内容:多继承下的构造函数,命名冲突在前面的例子中,派生类都只有一个基类,称为 单继承(Single Inheritance)。除此之外, C++也支持 多继承(Multiple Inheritance),即一个派生类可以有两个或多个基类。 多继承容易让代码逻辑复杂、思路混乱,一直备受争议,中小型项目中较少使用,后来的 Java、 C#、 PHP 等干脆取消了多继承。 多继承的语法也很简单,将多个基类用逗号隔开即可。例如已声明了类A

    • 继承是面向对象编程的一个重要的方式,因为通过继承,子类就可以扩展父类的功能。 回忆一下Animal类层次的设计,假设我们要实现以下4种动物: Dog - 狗狗; Bat - 蝙蝠; Parrot - 鹦鹉; Ostrich - 鸵鸟。 如果按照哺乳动物和鸟类归类,我们可以设计出这样的类的层次: 但是如果按照“能跑”和“能飞”来归类,我们就应该设计出这样的类的层次: 如果要把上面的两种分类都包含进来

    • 继承是面向对象编程的一个重要的方式,因为通过继承,子类就可以扩展父类的功能。 回忆一下Animal类层次的设计,假设我们要实现以下4种动物: Dog - 狗狗; Bat - 蝙蝠; Parrot - 鹦鹉; Ostrich - 鸵鸟。 如果按照哺乳动物和鸟类归类,我们可以设计出这样的类的层次: ┌───────────────┐ │

    • 问题内容: 为了完全理解如何解决Java的多重继承问题,我有一个经典的问题需要澄清。 可以说我有类此有子类和我需要做一个类,从扩展和自既是一只鸟和一匹马。 我认为这是经典的钻石问题。从我能理解经典的方式来解决,这是使,和类接口,并实现从他们。 我想知道是否还有另一种方法可以解决仍然可以为鸟和马创建对象的问题。如果有一种能够创造动物的方法,那也很棒,但不是必须的。 问题答案: 你可以为动物类(生物学

    • 假设我有类,它有子类和,我需要创建一个类,它扩展自和,因为既是鸟也是马。 我想这就是经典的钻石问题。据我所知,解决这个问题的经典方法是将、和类作为接口,并从它们实现。 我想知道是否有另一种方法来解决这个问题,在这个方法中,我仍然可以为鸟和马创建对象。如果有一种方法也能创造动物,那将是伟大的,但不是必要的。

    • 本章前面讨论了单一继承,即一个类是从一个基类派生来的。一个类也可以从多个基类派生而来,这种派生称为“多重继承”(multiPle inheritance)。多重继承意味着一个派生类可以继承多个基类的成员,这种强大的功能支持了软件的复用性,但可能会引起大量的歧义性问题。 编程技巧 9.1 多重继承使用得好可具有强大的功能。当新类型与两个或多个现有类型之间存在”是”关系时(即类型A“是”类型B并且也“