【问题标题】:What is multiple re-inheritance?什么是多重再继承?
【发布时间】:2013-12-19 23:43:33
【问题描述】:

我将以下称为“多重再继承”:

  • 直接继承一个类一次,并通过继承其一个或多个后代间接继承一次或多次
  • 通过继承一个类的两个或多个后代来间接继承一个类两次或多次

我想知道它是否存在以及如何明确地访问嵌入的子对象。

1.) [Professional C++, 2nd ed.] 声明可编译程序不能有直接继承的类它的直接父级和所述父级的父类。是真的吗?

给定一个扩展GrandParentGrandParentParent,VC12 和g++ 允许GrandChild 直接从ParentGrandParent 继承。在 VC12 和 g++ 中,可以这样定义这些类:

GrandParent 声明了一个 int num 数据成员。 Parent 除了继承GrandParentnum 之外,还声明了自己的numGrandChild 除了继承ParentGrandParentnums 之外,还声明了自己的num

VC12 似乎允许全面明确的成员访问,但 g++ 仅在某些情况下允许。

#include <iostream>
using std::cout;
using std::endl;

struct GrandParent { int num; };
struct Parent : GrandParent { int num; };
struct GrandChild : GrandParent, Parent { int num; };

int main()
{
    GrandChild gc;
    gc.num = 2;
    gc.Parent::num = 1;
    gc.Parent::GrandParent::num = 0; // g++ error: ‘GrandParent’ is an ambiguous base of ‘GrandChild’
    gc.GrandParent::num = 5;         // g++ error: ‘GrandParent’ is an ambiguous base of ‘GrandChild’

                                                 // --VC12 output; g++ output--
    cout << gc.num << endl;                      // 2 ; 2
    cout << gc.Parent::num << endl;              // 1 ; 1
    cout << gc.Parent::GrandParent::num << endl; // 0 ; N/A due to above error
    cout << gc.GrandParent::num << endl;         // 5 ; N/A due to above error
}

2.) 为什么 (a) gc.Parent::GrandParent::num 在 g++ 中是模棱两可的,而 (b) gc.Parent::num 不是? (a) 唯一地描述了它在继承树上的位置。 gc 只有 1 个 Parent 子对象,它只有 1 个 GrandParent 子对象,它只有 1 个 num。对于 (b),gc 有一个 Parent,它有自己的 num,还有一个 GrandParent 子对象和另一个 num

3.) 对于gc.GrandParent::num,VC12 似乎在查看gc 的直接GrandParent 基子对象以获取后者的num。我猜它是明确的原因是它是由gc 限定的名称查找,因此. 右侧的实体首先在gc 的范围内查找,最直接的GrandParentgc 的作用域是直接继承的,不是通过Parent 间接继承的。我错了吗?

4.) 为什么 gc.GrandParent::num 对 g++ 模棱两可,而 gc.Parent::num 不是?如果一个是模棱两可的,那么两者不应该同样模棱两可吗?对于先前,gc 有两个GrandParents;对于后者,Parent 有 2 个nums。


Gregoire、Marc R. 等人。 专业 C++,第 2nd 版。印第安纳波利斯:威利出版社,2011 年。 241. 打印。

【问题讨论】:

  • "[Professional C++, 2nd ed.]† 指出可编译程序不能有一个类直接继承其直接父类和所述父类的父类。是真的吗?" 不,那是错误的。 C++ 标准甚至包含一个示例并在其中声明它是格式良好的,请参阅 [class.mi]/3 class D : public A, public L { void f(); /∗ ... ∗/ }; // well-formed,其中 class A : public L { /∗ ... ∗/ };
  • 在同一段中,注释说:“一个类可以多次作为间接基类,并且可以是直接基类和间接基类。这样可以做的事情有限一个类。不能在派生类的范围内引用直接基类的非静态数据成员和成员函数。”(强调我的)。
  • 我认为这些访问不明确的原因是GrandChild::Parent::GrandParent 在名称查找方面等同于GrandChild::GrandParent。同样,struct A { int x; }; struct B : A {}; struct C : A {}; struct D : B, C {}; D d; d.B::A::x = 42; 失败。
  • “如何明确地访问嵌入的子对象”你可以通过类似static_cast&lt;Parent&amp;&gt;(gc).GrandParent::num = 42;
  • +1 有趣的问题。添加到编译器组合中,Apple LLVM 版本 5.0 (clang-500.2.79)(基于 LLVM 3.3svn)报告与 gcc 相同的歧义错误。

标签: c++ inheritance multiple-inheritance diamond-problem


【解决方案1】:

对此的常用术语是菱形图案(或diamond problem)。

这本身不是错误,但正如此处的 cmets 中所指出的,任何尝试访问在层次结构中的其他位置重复的直接基础都将导致歧义错误。

一种解决方法是使基础间接。 C++11 中新的继承构造函数特性允许完美的包装器:

template< typename base, typename tag >
struct disambiguated_base : base
    { using base::base; };

给定一个未使用的标签类型,这会生成一个新类,该类派生自给定的基类,并且在功能上与给定的基类相同。标记类型可能是由详细类型说明符表示的不完整类:

struct GrandChild : Parent,
    disambiguated_base< GrandParent, class grandchild_grandparent_tag > {

    typedef disambiguated_base< GrandParent, grandchild_grandparent_tag >
        my_direct_grandparent;

    int num;
};

现在GrandChild 可以使用my_direct_grandparent:: 来消除成员访问的歧义。

【讨论】:

  • +1。巧妙。但是,我也想访问GrandChildParentGrandParent。为详细类型说明符想出另一个名称(如 grandchilds_parents_grandparent)会很麻烦,尤其是如果钻石得到更多代。是否可以将其编入索引以允许GrandChild.disambiguator&lt;0&gt;::GrandChild.disambiguator&lt;1&gt;::?即disambiguated_base的类型tag能否改为模板模板参数,其模板参数为int N?不完整的类模板foo&lt;N&gt; 将如何在详细类型说明符中表示?谢谢。
  • @CodeBricks 是的,标签可以是任何东西,包括非类型,如果你这样声明的话。不完整的特化Foo&lt;N&gt; 简称为Foo&lt;N&gt;,但您确实需要声明template&lt; int N &gt; class Foo;。但是,除了直接基础之外,您不需要消除歧义。 Parent::GrandParent::my_direct_grandparent:: 一起工作正常。
  • *表示disambiguator&lt;N&gt; 而不是foo&lt;N&gt;gc.Parent::GrandParent:: 给出 g++ 错误。 GrandParentGrandChild 的直接基类。如果我这样做template&lt; typename base, template &lt;int N&gt; class tag &gt; struct disambiguated_base... 那么我如何在GrandChild 的继承语句中对一个不完整但可索引的类模板进行相应的更改? struct GrandChild : Parent, disambiguated_base&lt; GrandParent, class template&lt;int N&gt; class grandchild_grandparent_tag &gt; ?
  • 您不需要更改任何声明,除了添加我在上一条评论中提到的模板前向声明。然后将其用作struct GrandChild : Parent, disambiguated_base&lt; GrandParent, disambiguator&lt; 3 &gt; &gt; {。标签可以是任何类型名称,例如intstringclass this_is_a_unique_namesome_template&lt; 42 &gt;
  • @CodeBricks 自动生成标签超出了这个问题的范围,可能根本不可能。最好打开一个新问题。
【解决方案2】:

我正在添加已接受的答案。它指出,如果派生类也间接继承 base,则派生类不能访问直接的 base 类。它的解决方案通过使用第二个类型参数为tag 的模板包装base 类来间接处理它。这确保了base 与派生类间接关联,前提是派生类使用唯一的tag 扩展了包装的基类。下面的示例将使用非类型 tag

如果类金刚石问题被概括为包含更多代的形式:

  • ith 类继承自 base 和 (i – 1)th,
  • (i – 1)th 继承自 base 和 (i – 2)th,
  • …和
  • 2nd继承自base

那么它是一个临时容器,其中每个元素都存储在每个唯一标记的base 中。在这种情况下,tag-making 应该是自动化的。一种方法是通过非类型模板合并所有派生类。它的非类型参数N可以指定递归继承迭代的次数。通过使tag 成为非类型参数,确定子类数量的参数值可以与标记每个子对象类型的参数值唯一相关。比如tag = 10对应N = 10,指的是层次上的第10代:

// disambiguated_wrapper.h

struct int_wrapper {
    int num;
};

template < typename base, unsigned int tag >
struct disambiguated_wrapper : base {
    using base::base;
};

// improvised_container.h

#include "disambiguated_wrapper.h"

template <unsigned int N>
struct improvised_container : 
    protected disambiguated_wrapper<int_wrapper, N>, 
    protected improvised_container<N - 1> {

    unsigned int size() const { return N; }

    int& at(const unsigned int index) {
        if (index >= N) throw "out of range";
        else return (index == N - 1) ?
            this->disambiguated_wrapper<int_wrapper, N>::num :
            this->helper(index);
    }
protected:
    int& helper(const unsigned int index) {
        return (index == N - 1) ?
            this->disambiguated_wrapper<int_wrapper, N>::num :
            this->improvised_container<N - 1>::helper(index);
    }
};
#include "specializations.h"

// specializations.h

template <>
struct improvised_container<0> {
    improvised_container() = delete;
}; // ^ prohibits 0-length container

template <>
struct improvised_container<1> : 
    protected disambiguated_wrapper<int_wrapper, 1> {

    unsigned int size() const { return 1; }

    int& at(const unsigned int index) {
        if (index != 0) throw "out of range";
        else return this->disambiguated_wrapper<int_wrapper, 1>::num;
    }
protected:
    int& helper(const unsigned int index) {
        if (index != 0) throw "out of range";
        else return this->disambiguated_wrapper<int_wrapper, 1>::num;
    }
};

// main.cpp

#include "improvised_container.h"
#include <iostream>
int main() {
    improvised_container<10> my_container;
    for (unsigned int i = 0; i < my_container.size(); ++i) {
        my_container.at(i) = i;
        std::cout << my_container.at(i) << ",";
    }   // ^ Output: "0,1,2,3,4,5,6,7,8,9,"
}

元素访问 at 不能递减 index 以递归调用自身,因为 index 不是编译时常量。但是N 是。所以,at 调用 helper,它递归地调用 (i - 1)th 子对象中自身的 (i​​ - 1)th 版本,递减 @987654347 @ 直到等于index – 1,每次调用都会将作用域移得更深,最后返回目标作用域的元素。它检查 index – 1 而不是 index,因为 0thimprovised_container 专业化的 ctor 是 deleted。 at 补差价。

improvised_container 使用protected 继承来防止客户端代码访问其基础子对象的atsize 方法。子对象的大小小于封闭对象的大小。

这适用于 g++ 4.8。继承构造函数using base::base会导致VC12报错,但是可以省略,因为元素类型是int

【讨论】:

  • 啊……我还没有完全理解这一点,但看起来你正在实现类似 std::tuple 的东西。
  • @Potatoswatter,我只是在底部添加了解释。老实说,我不知道std::tuple 的实现。
  • 各种实现都是可能的,包括碱基链。关键特性是它返回给定索引的子对象。尽管您的 getter 在运行时工作,但我认为它也可以调整为添加编译时变体。
  • 我不明白最后的评论...我不是建议或批评任何东西,只是提到实现细节与元组的相似性。
  • 哦,我不知道您对编译时变体感兴趣。由于std::tuple 通常已经针对您使用的任何编译器进行了调整,因此最好不要重新实现它。无论如何,碱基链并不是特别有效。我不知道您的用例是什么,我只是在真空中提及相似之处并回答问题,没有应用上下文。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2015-04-28
  • 2020-02-10
  • 2020-04-07
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多