views:

166

answers:

5

Background: I am working on a framework that generates C++ code based on an existing Java class model. For this reason I cannot change the circular dependency mentioned below.

Given:

  • A Parent-Child class relationship
  • Parent contains a list of Children
  • Users must be able to look up the list element type at run-time

I've modeled this in the following testcase:

Main.cpp

#include "Parent.h"

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

int main(int argc, char* argv[])
{
    Parent parent;
    cout << Parent::getType() << endl;
    cout << parent.getChildren().getType() << endl;
    return 0;
}

Parent.h

#ifndef PARENT_H
#define PARENT_H

#include <string>

#include "Array.h"
class Child;

class Parent
{
public:
    Array<Child> getChildren()
    {
        return Array<Child>();
    }

    static std::string getType()
    {
        return "parent";
    }
};

#endif

Child.h

#ifndef CHILD_H
#define CHILD_H

#include "Parent.h"

class Child: public Parent
{
};

#endif

Array.h

template <typename ElementType>
class Array
{
public:
    static std::string getType()
    {
        return ElementType::getType();
    }
};
  1. When I compile the above code I get: error C2027: use of undefined type 'Child' at return ElementType::getType();

  2. If I try #include "Child.h" instead of the forward declaration I get: error C2504: 'Parent' : base class undefined at class Child: public Parent

  3. If I try Array<Child*> instead of Array<Child> I get: error C2825: 'ElementType': must be a class or namespace when followed by '::' at return ElementType::getType();

The circular dependency comes about because:

  1. Child.h needs to know about class Parent
  2. Parent.h needs to know about class Array
  3. Array.h needs to know about class Child

Any ideas?

+4  A: 

One way to resolve this problem is to separate the implementations from the interfaces.

So, put the implementation of Parent into a .cpp file, so that the compiler can see the definitions of Parent, Child and Array when compiling Parent::getChildren().

#ifndef PARENT_H
#define PARENT_H
#include <string>
#include "Array.h"

class Child;

class Parent
{
public:
    Array<Child> getChildren();
    static std::string getType();
};

#endif

And in parent.cpp:

#include "parent.hpp"
#include "child.hpp"

Array<Child> Parent::getChildren() {
    return Array<Child>();
}

// etc.

Update:

Yes, the actual problem is caused by Array::getType() being instantiated without a definition of Child being present, so my solution is incomplete.

Pete Kirkham's solution is good: just include child.hpp into main.

For a interface/implementation separation to work, a separate implementation file for Array would be required with explicit instantiation of Array and any other required instantiations. This is probably not what you want, but for completeness, it would look something like:

In array.hpp:

#ifndef ARRAY_HPP
#define ARRAY_HPP

#include <string>

template <typename ElementType>
class Array
{
public:
    static std::string getType();
};

#endif

And in array.cpp:

#include "array.hpp"
#include "child.hpp"

template<typename ElementType>
std::string Array<ElementType>::getType()
{
    return ElementType::getType();
}

template class Array<Child>;
janm
+1 for separate implementation from interface.
5ound
@janm, I don't think your solution applies for this question. The compiler is complaining about "return ElementType::getType()" not "Parent::getChildren()" and the former cannot go into a cpp file because it uses templates.
Gili
@Gili: No, this is the right way. The error message is caused "proximally" by `return ElementType::getType()`, but the underlying cause is `Parent::getChildren()` -- *that* is what is causing instantiation of `Array<Child>`, which fails because we haven't seen the definition of `Child` yet. You must wait until `Child` has been defined before instantiating `Array<Child>`, implying that the definition of `Parent::getChildren()` *must* be non-inline.
j_random_hacker
@Gili: In fact Pete Kirkham has demonstrated that an inline definition works (which baffles me) so I must eat my words... :/
j_random_hacker
@j_random_hacker, you were on the right track but it looks like the template is being instantiated by main(), not Parent::getChildren(). Moving Parent::getChildren() to the CPP file did not help.
Gili
I agree; see updated answer.
janm
A: 

Maybe you could use pointer to Child in Parent instead? Something like

#ifndef PARENT_H
#define PARENT_H

#include <string>

#include "Array.h"
class Child;

class Parent
{
public:
    Array<Child*> getChildren()
    {
        return Array<Child*>();
    }

    static std::string getType()
    {
        return "parent";
    }
};

#endif

Or, more generally, maybe it is possible to use compiler firewall technique (a.k.a. opaque pointer, a.k.a. PIMLP). More info about it here.

Haspemulator
Using a pointer to Child doesn't help. As the question explains, I get error C2825. I assume this means that more general PIMPL techniques won't help either?
Gili
If you'll change definition of class Array as mentioned in j_random_hacker post, it should work, i.e. error C2825 should be gone.
Haspemulator
+1  A: 

EDIT: The OP has edited the question to remove the infinite-size-data-structure problem noticed by myself and rlbond. With this change, it's now possible to use Array<Child> instead of Array<Child*>, as janm's answer shows.

Change Array<Child> to Array<Child*>, and alter the Array type to understand that it contains pointers to objects instead of the objects themselves:

New Array.h

// E.g. strip_pointer_from<Foo*>::type is Foo
template <typename T>
struct strip_pointer_from<T> {};

template <typename T>
struct strip_pointer_from<T*> {
    typedef T type;
};

template <typename ElementType>
class Array
{
public:
    static std::string getType()
    {
        return typename strip_pointer_from<ElementType>::type::getType();
    }
};

I would strongly recommend rethinking Array, though -- is there any way you can use a plain vector<Child> and just ask each element for its type?

j_random_hacker
Your addendum in not correct - for the code as given, no T are stored in Array<T> at all, so there is no recursive storage. If the fleshed-out Array<T> functions in a similar manner to std::vector<T>, then Parent contains zero or more Child objects, initially zero, with storage for further children allocated dynamically as required. struct Node { std::vector<Node> children; } is perfectly legal, if rather prone to reallocation effects.
Pete Kirkham
@Pete: You're right -- the OP changed the error message I referred to in that addendum after I wrote that. Now that it's of finite size, `Array<Child>` works fine as janm's answer shows.
j_random_hacker
A: 

The following is how I've tended to solve the problem in systems requiring metaclass information. See also other answer for a more direct solution to your problem.


The pattern used in the stl is to use a type to represent a type, not a string. So std::vector<T>::value_type represents the type stored in the vector. It is then up to the client code to make use of this type.

If you want the runtime type of the object, use a base class which has a virtual function that returns the type. For the static type of a place, you can use partial specialisation:

Object.h

#ifndef OBJECT_H
#define OBJECT_H

#include <string>

template <typename T>
struct type_info {
    // extend type_info<void*> to inherit defaults
    const static bool is_array = false;
};

template <typename T>
std::string type_name ( const T& )
{
    return type_info<T>::name();
};

template <typename T>
std::string type_name ()
{
    return type_info<T>::name();
};

#endif

Parent.h

#include "Object.h"
#include "Array.h"

class Child;
class Parent
{
public:
    Array<Child> getChildren() {
        return Array<Child>();
    }
};

template <>
struct type_info <Parent> : public type_info<void*> {
    static std::string name () {
        return "parent";
    }
};


#endif

Array.h

template <typename ElementType>
class Array
{
public:
    typedef ElementType value_type;
};

template <typename T>
struct type_info <Array<T > > {
    static std::string name () {
        return "Array<" + type_name<T>() + ">";
    }

    const static bool is_array = true;
};

Child.h

#ifndef CHILD_H
#define CHILD_H

#include "Parent.h"

class Child: public Parent
{
};

template <>
struct type_info <Child> : public type_info<void*> {
    static std::string name () {
        return "child";
    }
};

#endif

Main.cpp

#include "Object.h"
#include "Parent.h"
#include "Child.h"

#include <iostream>
#include <iomanip>

using std::cout;
using std::endl;
using std::boolalpha;

template<typename T> bool type_is_array (const T&) { return type_info<T>::is_array; }

int main(int argc, char* argv[])
{
    Parent parent;
    cout << type_name<Parent>() << endl;
    cout << type_name(parent.getChildren()) << endl;
    cout << boolalpha << type_is_array(parent) << endl;
    cout << boolalpha << type_is_array(parent.getChildren()) << endl;
    return 0;
}
Pete Kirkham
+3  A: 

The error is due to the Child class not being present when the template is instantiated.

Add the following either to Main or at the end of Parent.h:

#include "Child.h"

This compiles fine with both g++ 4 and VS 2010.

Pete Kirkham
As the question indicates, you will get error C2504.
Gili
@Gili The question talks about include child.h before the definition of parent. Including it *after* the definition solves the problem.
Pete Kirkham
@Pete, My mistake. You are right!
Gili
So it does! OK, my understanding of when a name is looked up clearly needs some tweaking. +1. (I'm confused by the fact that the compiler will see the body of `Parent::getChildren()`, and thus must instantiate `Array<Child>`, before it has seen the definition of `Child`. Can you explain? (To save me grovelling through the standard...))
j_random_hacker
@j_random_hacker it will see a declaration, but it only has to resolve `Child::GetTypeName()` where it's used. Hence the error points at the line in main which calls `parent.getChildren().getType()` rather than at anywhere in the headers.
Pete Kirkham
@Pete: Thanks, not sure I fully get it yet but will try thinking about it when I get time.
j_random_hacker