多重继承的运算符()

时间:2017-06-08 07:34:27

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

首先,考虑一下这个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 outboth are uncommented的情况下也是如此。但是,如果你只注释掉一个而不是另一个,那么事情就是suddenly different again:现在只有MSVC会像我期望的那样抱怨,而Clang和gcc都认为它很好 - 并且使用两个继承的成员重载决议,尽管using只引入了一个!

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

3 个答案:

答案 0 :(得分:7)

巴里排名第一。你的#2命中了一个角落:无捕获的非泛型lambdas有一个隐式转换为函数指针,它在不匹配的情况下使用。也就是说,给定

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]

答案 1 :(得分:4)

只有在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

答案 2 :(得分:0)

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

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

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

但在您的代码中,名称查找失败。该名称未在DECLARE @object int; DECLARE @hr int; DECLARE @src varchar(255), @desc varchar(255); EXEC @hr = sp_OACreate 'SQLDMO.SQLServer', @object OUT; IF @hr <> 0 BEGIN EXEC sp_OAGetErrorInfo @object, @src OUT, @desc OUT raiserror('Error Creating COM Component 0x%x, %s, %s',16,1, @hr, @src, @desc) RETURN END; GO 中声明,因此将搜索基类。如果在多个直接基类中找到该名称,则该程序格式错误,并且错误消息将其描述为不明确的名称。

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

foo

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

  

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

     

postfix-expression(expression-list opt)

     

其中postfix-expression计算为类对象,而可能为空的表达式列表与该类的operator()成员函数的参数列表匹配。因此,如果operator()存在,并且如果通过重载解析机制将运算符选为最佳匹配函数,则对于类型为T的类对象x,调用x(arg1,...)将被解释为x.operator()(arg1, ...)(13.3) 0.3)。

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

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

  1. T::operator()operator()中声明。
  2. T范围内的operator()的无限制查找成功,并对具有给定参数的查找集执行重载解析成功。
  3. 调用上下文中T的合格查找成功,并在给定参数成功的情况下对该查找集执行重载解析。
  4. 别的什么?
  5. 从这里开始(无论如何),我想理解为什么标准并不是简单地说T::operator()意味着f(123),前者是不正确的,当且仅当后者是不正确的。实际措辞背后的动机可能会揭示意图,因此编译器的行为符合意图。