3

I am writing a backend of a compiler of a subset of Java. The backend writes C++ code. There is some hypothetical Java code, though, that I do not known how to translate to C++.

An example problem is shown in the following code. A is extended by B, B is extended by C, and here are respective three header files A.h, B.h and 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) */

As can be seen, B overrides A's method get(). The overriding method returns a pointer to a respective subclass, which I guess is valid in C++, thanks to covariance.

What I do not know, is the way of informing the compiler, that C is indeed a subclass of B, and thus, that the overriding method is valid.

A forward declaration of C within B.h, just as the one seen in the code, is not enough, as it says nothing about C's superclasses.

An inclusion of C.h within B.h would be circular, as C.h already includes B.h. The latter is needed, because it is not enough to have only a forward declaration of the superclass.

What can be done with that?

EDIT Two remarks.

1 One of the poster claims, that the following is impossible in Java, so I add a Java version:

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

It compiles just fine.

2 I do not insist on compiling such somewhat strange cases. If they can not be translated to a readable code in C++, then fine, the backend will just fail with an error message. In fact, I am more interested in a general way of resolving the circular dependencies like that in C++.

EDIT 2

Thank you all, I am impressed by the efficiency of this site.

I concluded that, because:

  1. the header files generated are going to be used by other programmers;
  2. guessing from your answers, there is likely no solution that produces simple, readable header files;
  3. circular references involving return types are probably rare;
  4. I avoid C++ because, amongst others, it allows for solutions like that -- I know C++ has its uses, but personally, I prefer languages that have a simpler grammar, like Java;

the backend will:

  1. use forward declarations where possible;
  2. otherwise it will use includes, checking if the latter is circular; if yes, it will fail with an error message.

Cheers, Artur

arataj
  • 373
  • 3
  • 12
  • Side comment: I don't think you can pull that concrete example in C++. For covariance to work, the compiler must know that the types are actually covariant, and that requires that in the definition of `B` class, `C` definition is present (if it is not, the compiler cannot know whether it is in fact covariant or just unrelated), but then, for `C` to be defined (and since it depends on `B` that is a base) `B` has to be previously defined. While some circular dependencies can be broken with forward declarations, you cannot do so with covariant return types. – David Rodríguez - dribeas Jul 06 '11 at 16:15
  • How c++ and java are fundamentally different is that c++ supports single-pass compilers, and thus requires forward declarations and placing classes in correct order in translation units. Forward decls are only needed when your dependency tree changes to dependency graph because of loops etc. This is very different from how java can handle it. – tp1 Jul 06 '11 at 16:42
  • The j2sdk 1.4 Java compiler is perhaps pretty old, but it refuses to compile your Java code, saying "b.java:2: get() in B cannot override get() in A; attempting to use incompatible return type". You write "one of the posters", I assume that means me, even though I have not made the claim that you attribute to the unspecified person. Please be more careful, be more precise, and don't make misleading statements and false claims. – Cheers and hth. - Alf Jul 06 '11 at 18:10
  • @Alf I did not want to offend you. You indeed just stated a supposition, but I, admittedly wrongly, have read it as a claim because of that "does not occur naturally in Java". And, you gave me a helpful answer, but for some reason it got "unchecked" once I "checked" another answer. Covariance of return types was introduced to Java 5. – arataj Jul 06 '11 at 18:20
  • @arataj: thanks, I didn't know that; I need to brush up on Java, evidently! – Cheers and hth. - Alf Jul 06 '11 at 18:24

4 Answers4

3

As you're generating code by machine, it's OK to use some dirty, filthy tricks.

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();
}
Puppy
  • 144,682
  • 38
  • 256
  • 465
  • I was thinking of an answer along these lines. But when the compiler needs to issue a call to one of the `get()` methods, it needs to decide *which one* to invoke. I think it's a solvable problem, provided that the header files for all three (or N) classes are visible in the scope in which the code is being generated. – Dan Breslau Jul 06 '11 at 16:03
  • @DeadMG This is clever, but unfortunately, the filth can use only used in the implementation files, as the header files are to be used by other programmers. – arataj Jul 06 '11 at 16:18
  • @arataj: Do you mean that the header files will be used by people writing their own C++ code? Generating C++ from Java is hard enough, without trying to make it programmer-friendly C++. – Dan Breslau Jul 06 '11 at 16:23
  • @arataj: You can't do that anyway, because there's no way to convert the GC of Java into C++'s memory management. @Dan: You generate the most derived call- e.g., if you have a B*, then you call CLASS_B_get(). – Puppy Jul 06 '11 at 17:23
  • @Dan @DeadMG I simulate destructors with finalize(). The Java programmers must thus care. I write the backend, because (1) I was asked to write a library in C++, and it is easier for me to use Java & Netbeans, (2) it is a good occasion to test the compiler I wrote; I found a bunch of bugs in it thanks to the discussed C++ support already. – arataj Jul 06 '11 at 17:42
  • @arataj: Destructors and `finalize()` are most completely not the same thing at all. `finalize` is asynchronous, non-deterministic and no other objects exist. Destructors are deterministic and synchronous and your member variables still exist. Try closing files in a finalizer and see how far you get. There are also Java things that have absolutely no counterpart in C++, like reflection, not just the GC. – Puppy Jul 06 '11 at 18:09
  • @DeadMG Yes, `finalize` is non-deterministic, this is why it does nothing but calling empty methods like `Mem.del(Object)` in the Java sources. The methods have a meaning only on the C++ side. By the way, this is not going to be a general compiler but it is instead intended only for translation of a subset of Java into graphs, it does not even support compiled libraries. The translation to C++ is a secondary use. – arataj Jul 06 '11 at 18:33
2

You can implement covariance yourself, cleanly, without relying on the compiler and without any casts. Here's a header with some circular dependencies:

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();    
};

You implement B::get_A in terms of B::get_C and C::get_A in terms of C::get_B. All get_* are implemented in the cxx file. You can because all three classes are already completely defined. The user always calls get().

Sorry if the formatting is wrong, I'm posting from a cell phone.

Edit: the solution with static casts is not always applicable, e.g. when virtual inheritance is involved (you will need dynamic casts then).

n. m. could be an AI
  • 112,515
  • 14
  • 128
  • 243
  • Your answer is the exact same as mine! – Puppy Jul 06 '11 at 22:53
  • @DeadMG: It's almost the same, yes. You don't use the public non-virtual interface. It's not essential but adds some clarity I think. But when I started writing my answer I didn't see yours, otherwise I would just comment on it. Took me an awful lot of time to finish on this tiny cell phone keyboard. – n. m. could be an AI Jul 07 '11 at 06:31
1

I'm not sure your explanation of the problem's origin (writing a compiler back-end) is truthful, because (1) the presented code is not even correct, lacking semicolons, and I think one who were writing compiler stuff would manage to present correct code, and (2) the problem does not naturally occur in Java code, and I'm not sure it can even be expressed directly in Java except by the workaround that I show below (in which case you would not need to ask), and (3) it's not a difficult problem, not a problem a compiler writer would struggle with.

That is, I strongly suspect that this is homework.

That said, you simply have to implement the covariance yourself, e.g. like this:

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

It's the same as implementing covariant smartpointer results, except that for smartpointer results one can more rely on C++ support for covariant raw pointer results.

It's a well-known technique.

Cheers & hth.,

Cheers and hth. - Alf
  • 142,714
  • 15
  • 209
  • 331
  • As I just checked, if the methods in question were X* get() and Y* get(), where X extends Y, the covariance can be done at the compiler level without any tricks, as simply as in Java. Still, your answer seems to be fit just for the strange case discussed here, so thanks. – arataj Jul 06 '11 at 17:04
  • I added a Java equivalent to the question. It does not require any workarounds like that. In fact, if there are no circular dependencies, it does not require any workarounds in C++ as well since 1998. – arataj Jul 06 '11 at 17:45
  • @arataj: You're right that for a *different question* a *different answer* would be sufficient, but that's pretty meaningless. Also, I cannot make your Java code compile. But I only have an old Java compiler. – Cheers and hth. - Alf Jul 06 '11 at 18:17
0

Serious design issue here.

For the virtual function feature to work, all the get() function in classes A, B and C should have the same signature.

Change everything to:

virtual A* get();

Then

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.

Is this what you want?

Sharath
  • 1,627
  • 2
  • 18
  • 34
  • That doesn't solve his problem at all, as the derived class must have the new signature. – Puppy Jul 06 '11 at 15:57
  • @Sharath As I already said to the other poster, the question is about a implementing a compiler backend, and not about design. Still, I disagree, that it is a design fault in general, that the overriding function returns a subtype, as it is a normal practice in Java. – arataj Jul 06 '11 at 16:06
  • What you are trying to do makes no sense in C++. I haven't used Java in a decade, so I can't comment on Java. – Sharath Jul 06 '11 at 16:13