与 [] 运算符和多重继承的歧义

Ambiguity with [] operator and multiple inheritance

本文关键字:多重继承 歧义 运算符      更新时间:2023-10-16

考虑以下类:

class Foo
{
    public:
    void operator [] (const std::string& s) { }
    void operator [] (std::size_t idx) { }
};

在这里,给定一个 Foo f 的实例,表达式 f[0] 不是模棱两可的,因为编译器选择第二个重载。 同样,表达式 f["abc"] 也不是模棱两可的,因为编译器选择第一个重载(因为const char*可转换为std::string(。

那么,为什么如果我们有两个基类,每个基类都有不同的重载,就会突然出现歧义呢?

假设我们有:

class Base1
{
    public:
    void operator [] (const std::string& s) { }
};
class Base2
{
    public:
    void operator [] (std::size_t idx) { }
};
class Derived : public Base1, public Base2
{ };

现在,如果我们说:

Derived d;
d[0];

编译器抱怨:

    error: request for member ‘operator[]’ is ambiguous
      d[0];
         ^
   note: candidates are: void Base2::operator[](std::size_t)
      void operator [] (std::size_t idx) { }
           ^
   note:                 void Base1::operator[](const string&)
      void operator [] (const std::string& s) { }

为什么两个运算符重载现在都在基类中这一事实会导致任何歧义? 有没有办法解决这个问题?

编辑:这可能是编译器错误(我正在使用GCC 4.8.1(

这不是重载解析的问题,而是 10.2 中定义的成员名称查找的问题。考虑一下(因为我宁愿不到处写operator[](:

struct base1 { void f(int); };
struct base2 { void f(double); };
struct derived : base1, base2 {};
int main() {
   derived d; d.f(0);
}

当查找f在后缀表达式d.f(0) 中开始时,它将首先查看derived并发现f根本没有解析为任何内容。 然后,10.2/5 要求查找并行地进行到所有基类,构造单独的查找集。在本例中,S(f,base1( = { base1::f } 和 S(f,base2( = { base2::f }。 然后按照 10.2/6 中的规则合并这些集合。第一个项目符号处理当其中一个集合为空或不同集合的查找以同一成员结尾时合并(考虑它命中了一个共同的基(。第二个项目符号很有趣,因为它适用于这里

10.2/6 子弹 2

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

也就是说,S(f,base1( 不同于 S(f,base2(,因此 S(f,derived( 成为无效的声明集。并且查找失败。

调用不明确,因为两个运算符不会重载。重载仅适用于在同一作用域中定义的名称。 Base1Base2 定义了两个不同的作用域,因此在派生类中,编译器只看到两个没有连接的相同名称。正如其他答案所说,克服这个问题的方法是使用适当的using声明将两个名称提升到派生类中;完成此操作后,编译器将在派生类的定义范围内看到这两个名称,并应用重载解析。

class Derived : public Base1, public Base2
{ 
    public:
    using Base1::operator[];
    using Base2::operator[];
};

使继承显式,因此编译器无需"选择基"。

TL;DR:尽管这两个函数都在候选集中,但候选集也无效,使程序格式不正确。 有关详细信息,请参阅dribeas的答案。

<小时 />

这两个功能显然都是可行的,因为:

f((size_t)0)

f((const char*)0)

是合法的,两个转换序列都是隐式的。

最初,这两个候选人并不模棱两可,因为一个比另一个转换更好。 编译器选择了只需要整体提升的那个。 由于整体促销比其他转化序列"更好",因此它胜出。

现在,两个候选人都需要指针向上投射。 现在,涉及向上和整体提升的转换顺序不再明显更好。 因此,编译器无法选择,并且报告歧义。(注意:我认为没有用户自定义转换的转换顺序应该还是更好的,那个候选f(Base2* implicit, size_t)应该还是会赢的......但现在要复杂得多,因为重载解析规则涉及多个参数的转换。

"using"声明允许this指针与标识转换而不是向上转换一起传递,因此同样,一个转换序列只是积分提升,哪个更好。

<小时 />

从第 13.3.1 节开始:

候选函数集可以包含要针对同一参数列表解析的成员函数和非成员函数。为了使参数和参数列表在此异构集中具有可比性,成员函数被视为具有一个额外的参数,称为隐式对象参数,它表示已为其调用成员函数的对象。出于重载解析的目的,静态和非静态成员函数都具有隐式对象参数,但构造函数没有。

同样,在适当的情况下,上下文可以构造一个参数列表,其中包含一个隐含的对象参数来表示要操作的对象。由于参数和参数在其各自的列表中按位置关联,因此约定是隐式对象参数(如果存在(始终是第一个参数,隐含对象参数(如果存在(始终是第一个参数。

在重载解析期间,隐含对象参数与其他参数没有区别。但是,隐式对象参数保留其标识,因为相应参数的转换应遵循以下附加规则:

  • 不能引入临时对象来保存隐式对象参数的参数;并且

  • 不能应用用户定义的转换来实现与其类型匹配。

您是否尝试过明确表示 Derived 会同时公开两者?

class Derived : public Base1, public Base2
{
public:
    using Base1::operator[];
    using Base2::operator[];
};

我不知道它是否有效,我这里只有视觉。