11

考虑以下类:

class Foo
{
    public:

    void operator [] (const std::string& s) { }

    void operator [] (std::size_t idx) { }
};

在这里,给定一个 的实例Foo f,表达式f[0]是没有歧义的,因为编译器选择了第二个重载。同样,表达式f["abc"]也没有歧义,因为编译器选择了第一个重载(因为 aconst char*可以转换为 an std::string)。

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

假设我们有:

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)

4

5 回答 5

9

这不是重载决议的问题,而是 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)成为无效的声明集。并且查找失败。

于 2013-10-09T16:17:55.133 回答
4

该调用是模棱两可的,因为这两个运算符不会重载。重载仅适用于在同一范围内定义的名称。Base1Base2定义两个不同的范围,因此在派生类中编译器只会看到两个没有联系的相同名称。正如其他答案所说,克服这个问题的方法是将两个名称都提升到具有适当using声明的派生类中;完成后,编译器会在派生类的定义范围内看到这两个名称,并应用重载决议。

于 2013-10-09T15:35:57.390 回答
2
class Derived : public Base1, public Base2
{ 
    public:
    using Base1::operator[];
    using Base2::operator[];
};

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

于 2013-10-09T15:32:54.460 回答
2

TL;DR:虽然这两个函数都在候选集中,但候选集也是无效的,使得程序格式错误。有关详细信息,请参阅dribeas 的答案


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

f((size_t)0)

f((const char*)0)

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

最初,这两位候选人并没有模棱两可,因为一位比另一位转换得更好。编译器选择了只需要一个整体提升的那个。由于积分提升比其他转换序列“更好”,因此它赢了。

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

“使用”声明允许this指针通过身份转换而不是向上转换来传递,因此一个转换序列再次只是整体提升,这更好。


从第 13.3.1 节开始:

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

类似地,在适当的时候,上下文可以构造一个参数列表,其中包含一个隐含的对象参数来表示要操作的对象。由于实参和形参在它们各自的列表中按位置关联,因此约定是隐式对象形参(如果存在)始终是第一个形参,而隐含对象形参(如果存在)始终是第一个形参。

在重载决议期间,隐含的对象参数与​​其他参数无法区分。但是,隐式对象参数保留其身份,因为相应参数的转换应遵守以下附加规则:

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

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

于 2013-10-09T15:40:29.163 回答
0

您是否尝试过明确地说 Derived 公开了两者?

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

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

于 2013-10-09T15:31:37.473 回答