【问题标题】:Ambiguity with [] operator and multiple inheritance [duplicate][] 运算符和多重继承的歧义[重复]
【发布时间】:2013-10-17 01:27:02
【问题描述】:

考虑以下类:

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)。

那么,如果我们有两个 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)

【问题讨论】:

  • Visual C++ 2010 不会抱怨。你用的是哪个编译器?
  • 你可能想看看this question
  • 我似乎记得读过一些关于向 C++ 添加显式 null 类型以解决这个确切问题的内容...
  • @DaoWen:我相信您正在考虑nullptr 以及int 和指针(尤其是char const*)之间的重载解析,但那不是一回事。
  • @Channel72:也许在辩论结束之前不要将答案标记为“已接受”?

标签: c++


【解决方案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)变成无效的声明集。并且查找失败。

【讨论】:

  • 我相信这与 10.2/13 中的示例相矛盾。尤其考虑调用f(0.0)。哦等等,没关系,它有 using-declarations 所以它不一样。
  • 我试图理解这个问题的那些段落,你说得很清楚,+1。
  • 要清楚,你的意思是第1条适用于第一次合并,将S(f, derived)替换为S(f, base1),然后你描述的冲突发生在从base2合并时发生,其中S(f, base2)S(f, derived)比较,现在是S(f, base1)?
  • 其实10.2甚至适用于运营商吗?我(和 clang)同意你关于命名成员函数的结论。
  • 啊,是的,13.3.1.2/3 遵循 13.3.1.1.1(在限定的上下文中),它指的是 10.2
【解决方案2】:

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

【讨论】:

  • 这不是错误信息所说的。
  • @PeteBecker:这很有趣。您是否可以告诉我/我们提及(或暗示)您所说的内容。
  • 第 13 条,[over]/1:“当为同一范围内的单个名称指定两个或多个不同的声明时,该名称称为重载。”第 3 段:“当在调用中使用重载函数名称时,通过比较参数的类型来确定引用哪个重载函数声明...... [t]他的函数选择过程称为重载解析,并在 13.3 中定义。”
  • 重载分辨率确实适用。请参阅第 13.3 节——重载决议适用于候选集,其中包括在多个范围内找到的名称,而不仅仅是重载。
  • Peter 虽然按照该定义从技术上讲它们不是重载,但重载决议确实适用。考虑一个不同的例子:ADL。从完全不同的命名空间中提取的函数被汇集在一起​​,重载解决方案从中挑选出最好的。标准中的措辞似乎是一个不幸的选择。
【解决方案3】:
class Derived : public Base1, public Base2
{ 
    public:
    using Base1::operator[];
    using Base2::operator[];
};

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

【讨论】:

  • 为什么需要这样做?编译器可以清楚地看到重载。
  • @Nawaz - 它们不是重载,因为它们没有在同一范围内定义。
  • @Nawaz 看来这一次,clang++ less 限制了,而且是正确的。参见例如这里有一个“相反”的情况:using and overloading a template member function of a base class?
  • @Pete:他们是候选集的成员。
【解决方案4】:

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


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

f((size_t)0)

f((const char*)0)

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

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

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

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


来自第 13.3.1 节:

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

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

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

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

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

【讨论】:

  • "Both 重载" 不,在第二种情况下,有 no 重载。
  • @LightnessRacesinOrbit:好的,候选人。这会导致重载分辨率。见 13.3
  • 这是一个完全错误的解释。
  • 答案是错误。这不是重载解析的问题,而是查找的问题。提供了一个解释它的答案(或试图解释它),但基本上当在派生类型中找不到该成员时,它会在不同的基础中查找并合并结果。不是 union 的合并会产生无效集合并失败。重载决议永远没有机会
  • @n.m.:已经试过了,但不知道是相信clang还是gcc。
【解决方案5】:

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

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

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

【讨论】:

  • 为什么需要这样做?编译器可以清楚地看到重载。
猜你喜欢
  • 2021-11-13
  • 2016-05-07
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多