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

时间:2013-10-09 15:27:19

标签: c++

考虑以下课程:

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) { }

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

编辑 :这可能是编译错误(我使用的是GCC 4.8.1)

5 个答案:

答案 0 :(得分: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 bullet 2

     

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

即, S(f,base1) S(f,base2)不同,因此 S(f,派生)变为无效的声明集。查找失败。

答案 1 :(得分:4)

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

答案 2 :(得分:2)

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

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

答案 3 :(得分:2)

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


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

f((size_t)0)

f((const char*)0)

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

最初,这两位候选人并不含糊,因为一位候选人的转换比另一位更好。编译器选择了仅需要整体提升的编译器。由于整体促销比其他转换序列“更好”,因此赢了。

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

“using”声明允许this指针通过身份转换而不是upcast传递,因此一次转换序列只是整数提升,这更好。


从第13.3.1节开始:

  

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

     

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

  

在重载解析期间,隐含的对象参数与​​其他参数无法区分。但是,隐式对象参数保留其标识,因为对相应参数的转换应遵守这些附加规则:

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

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

  •   

答案 4 :(得分:0)

您是否尝试过明确表示Derived会同时曝光?

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

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