【问题标题】:A C++ covariance/overriding/circularity problemC++ 协方差/覆盖/循环问题
【发布时间】:2011-07-06 15:44:09
【问题描述】:

我正在编写 Java 子集的编译器的后端。后端编写 C++ 代码。不过,有一些假设的 Java 代码,我不知道如何翻译成 C++。

以下代码显示了一个示例问题。 A由B扩展,B由C扩展,下面分别是三个头文件A.h、B.h和C.h:


#ifndef A_H
#define A_H

class B;

class A {
  public: virtual B* get();
}

#endif /* !defined(A_H) */
==========================
#ifndef B_H
#define B_H

#include "A.h"

class C;

class B : public A {
  public: virtual C* get();
}

#endif /* !defined(B_H) */
==========================
#ifndef C_H
#define C_H

#include "B.h"

class C : public B {
}

#endif /* !defined(C_H) */

可以看出,B 覆盖了 A 的方法 get()。重写方法返回一个指向相应子类的指针,我猜这在 C++ 中是有效的,这要归功于协方差。

我不知道的是通知编译器的方式,C 确实是 B 的子类,因此,覆盖方法是有效的。

正如在代码中看到的那样,在 B.h 中对 C 进行前向声明是不够的,因为它没有说明 C 的超类。

在 B.h 中包含 C.h 将是循环的,因为 C.h 已经包含 B.h。后者是必需的,因为只有超类的前向声明是不够的。

可以用它做什么?

编辑两句话。

1 一位发帖人声称,以下内容在 Java 中是不可能的,所以我添加了一个 Java 版本:

A.java:


class A {
  public B get() {
    return null;
  }
}

B.java:


class B extends A {
  public C get() {     
    return null;
  }
}

C.java:


class C extends B {
}

它编译得很好。

2 我不坚持编译这种有些奇怪的案例。如果它们不能被翻译成 C++ 中的可读代码,那么很好,后端将失败并显示错误消息。事实上,我对解决循环依赖的一般方法更感兴趣,比如在 C++ 中。

编辑 2

谢谢大家,这个网站的效率给我留下了深刻的印象。

我的结论是,因为:

  1. 生成的头文件将被其他程序员使用;
  2. 根据您的回答猜测,可能没有解决方案可以生成简单、可读的头文件;
  3. 涉及返回类型的循环引用可能很少见;
  4. 我避免使用 C++,因为除其他外,它允许这样的解决方案—— 我知道 C++ 有它的用途,但就我个人而言,我更喜欢语法更简单的语言,比如 Java;

后端将:

  1. 尽可能使用前向声明;
  2. 否则它将使用包含,检查后者是否是循环的;如果是,它将失败并显示错误消息。

干杯, 阿图尔

【问题讨论】:

  • 旁注:我认为您无法在 C++ 中提取具体示例。要使协变起作用,编译器必须知道类型实际上是协变的,这就要求在B 类的定义中存在C 定义(如果不存在,编译器无法知道它实际上是否存在)协变或只是不相关),但是,对于要定义的C(并且因为它依赖于作为基础的BB 必须事先定义。虽然前向声明可以打破一些循环依赖关系,但协变返回类型不能这样做。
  • c++ 和 java 的根本区别在于 c++ 支持单遍编译器,因此需要前向声明并在翻译单元中以正确的顺序放置类。仅当您的依赖关系树由于循环等原因更改为依赖关系图时才需要前向 decls。这与 java 处理它的方式非常不同。
  • j2sdk 1.4 Java 编译器可能已经很老了,但它拒绝编译您的 Java 代码,说“b.java:2: get() in B cannot override get() in A;试图使用不兼容的返回类型”。你写了“其中一张海报”,我认为这意味着我,即使我没有声称你归因于未指明的人。请更加小心,更加准确,不要做出误导性陈述和虚假声明。
  • @Alf 我不想冒犯你。您确实只是提出了一个假设,但我承认错误地将其解读为一种说法,因为“在 Java 中不会自然发生”。而且,您给了我一个有用的答案,但是由于某种原因,一旦我“检查”了另一个答案,它就“未检查”。 Java 5 引入了返回类型的协变。
  • @arataj:谢谢,我不知道;显然,我需要复习 Java!

标签: c++ covariance circular-dependency overriding


【解决方案1】:

当您通过机器生成代码时,可以使用一些肮脏、肮脏的技巧。

class B;

class A {
  public: virtual B* CLASS_A_get();
}

class C;

class B : public A {
  public:
    virtual B* CLASS_A_get();
    virtual C* CLASS_B_get();
}

class C : public B {
}

// In B's .cpp file, you can include C.h
#include "C.h"
B* B::CLASS_A_get() {
    return CLASS_B_get();
}

【讨论】:

  • 我正在考虑这些方面的答案。但是当编译器需要调用get() 方法之一时,它需要决定调用哪一个。我认为这是一个可以解决的问题,前提是所有三个(或 N 个)类的头文件在生成代码的范围内都是可见的。
  • @DeadMG 这很聪明,但不幸的是,脏东西只能在实现文件中使用,因为头文件是供其他程序员使用的。
  • @arataj:你的意思是头文件将被人们编写自己的 C++ 代码使用吗?从 Java 生成 C++ 已经够难的了,无需尝试使其对程序员友好的 C++。
  • @arataj:无论如何你都不能这样做,因为没有办法将Java的GC转换成C++的内存管理。 @Dan:您生成最派生的调用-例如,如果您有 B*,那么您调用 CLASS_B_get()。
  • @Dan @DeadMG 我用 finalize() 模拟析构函数。 Java 程序员因此必须关心。我写后端,因为(1)我被要求用 C++ 写一个库,而且我使用 Java 和 Netbeans 更容易,(2)这是一个测试我写的编译器的好机会;由于已经讨论过的 C++ 支持,我在其中发现了一堆错误。
【解决方案2】:

您可以自己干净利落地实现协方差,无需依赖编译器,也无需任何强制转换。这是一个带有一些循环依赖的标题:

class A
{
  public:
    A* get() { return get_A(); }
  private:
    virtual A* get_A();
};

class B : public A
{
  public:
    C* get() { return get_C(); }
  private:
    virtual A* get_A();
    virtual C* get_C();    
};

class C : public A
{
  public:
    B* get() { return get_B(); }
  private:
    virtual A* get_A();
    virtual B* get_B();    
};

您根据 B::get_C 实现 B::get_A,根据 C::get_B 实现 C::get_A。所有 get_* 都在 cxx 文件中实现。你可以,因为这三个类都已经完全定义好了。用户总是调用 get()。

抱歉,如果格式错误,我是用手机发帖的。

编辑:静态转换的解决方案并不总是适用,例如当涉及到虚拟继承时(你将需要动态转换)。

【讨论】:

  • 你的答案和我的一模一样!
  • @DeadMG:几乎一样,是的。您不使用公共非虚拟接口。这不是必需的,但我认为增加了一些清晰度。但是当我开始写我的答案时,我没有看到你的答案,否则我只会评论它。我花了很长时间才完成这个小巧的手机键盘。
【解决方案3】:

我不确定您对问题根源的解释(编写编译器后端)是否真实,因为 (1) 提供的代码甚至不正确,缺少分号,我认为编写编译器的人会设法呈现正确的代码,并且(2)问题不会自然发生在 Java 代码中,我不确定它甚至可以直接用 Java 表达,除非我在下面显示了解决方法(在这种情况下,您不需要去问),并且(3)这不是一个难题,也不是编译器编写者会遇到的问题。

也就是说,我强烈怀疑这是家庭作业

也就是说,您只需自己实现协方差,例如像这样:

class B;

class A
{
private:
    virtual B* virtualGet() { ... }
public:
    B* get() { return virtualGet(); }
};

class C;

class B
    : public A
{
private:
    virtual B* virtualGet() { ... }
public:
    C* get() { return static_cast<C*>( virtualGet() ); }
};

class C
    : public B
{};

这与实现协变智能指针结果相同,除了智能指针结果可以更多地依赖 C++ 对协变原始指针结果的支持。

这是一种众所周知的技术。

干杯,

【讨论】:

  • 正如我刚刚检查的那样,如果有问题的方法是 X* get() 和 Y* get(),其中 X 扩展了 Y,则协方差可以在编译器级别完成而无需任何技巧,如就像在Java中一样。不过,您的答案似乎只适合这里讨论的奇怪案例,所以谢谢。
  • 我添加了一个与问题等效的 Java。它不需要任何类似的解决方法。事实上,如果没有循环依赖,那么自 1998 年以来,它也不需要 C++ 中的任何变通方法。
  • @arataj:你说得对,对于 不同的问题不同的答案 就足够了,但这毫无意义。另外,我无法编译您的 Java 代码。但我只有一个旧的 Java 编译器。
【解决方案4】:

这里有严重的设计问题。

要使虚函数功能起作用,A、B 和 C 类中的所有 get() 函数都应该具有相同的签名。

将所有内容更改为:

虚拟A* get();

然后

A* ptr1 = new B;
ptr1->get(); // this will call the get function of class B.

A* ptr2 = new C;
ptr2->get(); // this will call the get function of class C.

这是你想要的吗?

【讨论】:

  • 这根本解决不了他的问题,因为派生类必须有新的签名。
  • @Sharath 正如我已经对另一位发帖人所说的那样,问题是关于实现编译器后端,而不是关于设计。不过,我不同意,一般来说,重写函数返回子类型是一种设计错误,因为这是 Java 中的正常做法。
  • 您尝试做的事情在 C++ 中毫无意义。十年没用过Java,所以无法评论Java。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2014-09-02
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多