Monday, 7 February 2011

C++ Forward Dependencies

Forward declarations in C++ can be the best thing ever, they let you decouple the declaration of a class you're working on now from the implementation of another which you happen to want to use; but havn't got around to writing yet, something like this:

#include
class B; // This is a forward declaration
class A
{
private:
B* m_PointerToB;
public:
A (const B* p_InstanceOfB) : m_PointerToB (const_cast(p_InstanceOfB))
{ // I know this is declared inline, but just for this explanation
} // don't worry about it
void Output () { std::cout << "Hello World" << std::endl; }
};

So I can carry on declaring and implementing this class, without worring about B.

However, there is a problem with all this, if your forward declares are in different namespaces and you're trying to include the classes which are forward defining one another from different header files, what do I mean?

Well, I declare Class A in a header and implementation file pairing e.g, a.h and a.cpp, they might look like this

-- A.h ---
#pragma once
#ifndef CLASSA
#define CLASSA
#include
namespace SpaceA
{
class B;
class A
{
private:
B* m_InstanceOfB;
public:
A (const B*= NULL);
void Output ();
};
}
#endif
-- A.cpp
#include "A.h"
namespace SpaceA
{
A::A (const B* p_PointerToB) : m_InstanceOfB (const_cast(p_PointerToB))
{
Output();
}
void A::Output ()
{
std::cout << "Hello World" << std::endl;
}
}

This all works fine, you can compile and even run an instance of Class A around a little, the next day however, when you come along and implement Class B properly, some rather odd, and pretty badly explained (by the compiler) messages will pop out at you.

These messages will ellude, vaguly to the problem that B is not a B.  And they will usually be listed against the compilation of tile A.h but I've also had A.cpp and B.h show up as the erroneous file the fingers of failed compile are pointing at.

So, lets look at the header for B, before we go any further:

-- B.h --
#pragma once
#ifndef CLASSB
#define CLASSB
#include
namespace SpaceB
{
class B
{
static void Describe(const B*);
};
}
#endif
-- B.cpp --
#include "B.h"
namespace SpaceB
{
void B::Describe (const B* PointerToB)
{
std::cout << "Pointer to B is at ref " << PointerToB << std::endl;
}
}

If you compile now, you'll still not see an issue, you'll not get a repot of a duplicate declaration, because the two declarations are in different namespaces, i.e. they are not just "B" and "B" they are infact "SpaceA::B" and "SpaceB::B".

What we need do now is imagine we're on the third day of this (lets face it arduously hard coding task), and we go back now to link more class "B" related functionality into A, thus:

-- A.h (version 2) --
#pragma once
#ifndef CLASSA
#define CLASSA
#include
#include "B.h"
using namespace SpaceB;
namespace SpaceA
{
class B;
class A
{
private:
B* m_InstanceOfB;
public:
A (const B*= NULL);
void Output ();
};
}
#endif
-- A.cpp (Version 2) --
#include "A.h"
namespace SpaceA
{
A::A (const B* p_PointerToB) : m_InstanceOfB (const_cast(p_PointerToB))
{
Output();
}
void A::Output ()
{
B::Describe(m_InstanceOfB);
}
}

And here things go wrong, if you miss the "using namespace SpaceB" on line 6 of your new A header file, then you will get a message that "void Describe (const B*)" is not declared for class B.

Lots of you will be shouting "obvious", but some of you will be happy that someone out here on the internet has pointed this little foil out, but the bigger problem here is that you fix the name space as above or thus:

SpaceB::B::Describe(m_InstanceOfB);

And you suddenly get another error, this time you get a message that type "B* can not be cast to type B*", some compilers will be a little more specific and report "const SpaceA::B* can not be cast to const SpaceB::B*" [two of the three compilers I tried did this latter option].

And unless you're eagle eyed you might be foxed by this error for a while, the problem is of course the amgiuity you have left in your code from the forward declaration of B in the file A.h.

You could remove the forward declare and #include "B.h", but this might introduce a cyclic include, and most certainly will be a build dependency we can avoid (avoiding build inter dependencies is good - so long as you test your code properly - but that is another topic).

So avoid any problems including or making a new build dependency we simply need to move the forward declare into the correct namespace, thus:

-- A.h (version 3) --
#pragma once
#ifndef CLASSA
#define CLASSA
#include
#include "B.h"
namespace SpaceB
{
class B;
}
using namespace SpaceB;
namespace SpaceA
{
class A
{
private:
B* m_InstanceOfB;
public:
A (const B*= NULL);
void Output ();
};
}
#endif

No comments:

Post a Comment