Cyclic dependencies and forward declaration

Hi guys,

I have a question to the following.
Imagine two classes A and B that use one another. The naive approach: have the respective header files include one another, i.e.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
 // File A.h 
#ifndef A_h 
#define A_h 

#include"B.h" 

//... rest of header A...

#endif 


// File B.h
#ifndef B_h
#define B_h

#include"A.h"

//... rest of header B...

#endif 


If in my main file I include first A.h then B.h, I believe what will happen is this (correct me if wrong):
From reading A.h, A.h is first labelled "defined", then B.h is opened and labelled "defined". In B.h, A.h should be included, but as it is defined already, this step is ignored. This means that the "rest of header B" part does not know the whole content of A.h which can lead to errors.

Now, the solution is supposed to come in form of forward declarations.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34

// File A.h 
#ifndef A_h 
#define A_h 

class B; // forward declaration 

class A { 
    public: 
        void use_B(const B&); 
        void some_A_method(); 

    // ...rest 
}; 

#endif


// File B.h
#ifndef B_h
#define B_h

class A; // forward declaration

class B {
    public:
        void do_something_with_A(const A&);
        void some_B_method();
    
    \\... rest
};

#endif


Now, in the .cpp files, one is to include both headers, eg.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// File A.cpp 
#include"A.h" 
#include"B.h" 

void A::use_B(const B& b)
{

b.some_B_method(); 
}

void A::some_A_method() { 

// and so on


But does this not mean that the forward declaration of class A is read after the definition of class A? Because first I include A.h, i.e. I define class A. Then I include B.h which holds a forward declaration of A.
Can one really have declarations after the definitions?

Another question: should one try to avoid cyclic dependencies?

Best,
PiF
Sure, I don't think there's anything wrong with having a superfluous declaration after the definition. As long it isn't trying to declare it in a conflicting way. It would cause a slightly more annoying #if pattern to have to be applied if that weren't allowed.
1
2
3
4
5
6
7
8
9
10
class A {
    
};

class A;

int main()
{
    A a;
}
is fine

The general problem with circular dependencies is that it creates a tight coupling between two supposedly different things. If possible, try to just design the tightly coupled things as one thing.
Last edited on
> But does this not mean that the forward declaration of class A is read after the definition of class A?

Redeclaration is fine. For example:

1
2
3
4
5
class A { /* ... */ }; // definition
class A ; // declaration
class A ; // declaration
class A ; // declaration
class A ; // declaration 



> should one try to avoid cyclic dependencies?

Yes. Particularly in large code bases.
Back to basics, the declarations (any type, normal, forward, or re) exist for a reason, remember what that was? It is so the compiler can generate a place-holder ... you are telling it that "this exists, trust me, you will see it sooner or later" and the compiler says "OK, I will wait for it". If it does not find it, you error out, but if it does, it goes back where it saw it before it knew what it was and puts it in there. If you think of compiling a program a little like a mostly done jigsaw puzzle, maybe ... you can see there is a missing piece, and you know what it looks like, but you haven't seen it yet. Ok, you can keep going, and when you find it, snaps right into place!

All that lets the compiler run through main.cpp ... it finds a header file called foo.h, which defines say a simple function, void foo(); ... so it knows there will be a function that takes no parameters, returns no values, and name is foo... and later, down in main, it sees foo(); and that is fine: it does not stop and error out saying foo does not exist, it just tables that and keeps going, eventually finishing the main cpp file and then it sees that you also want to compile foo.cpp as part of the project. It goes through that, finds void foo() {stuff;} and connects all the dots.

Putting another declaration somewhere just moves that assist 'forward' with the goal STILL BEING that the compiler sees the declaration in time to resolve a 'use' (call to, object of, whatever) of the item. Because it saw that declaration before the use, it works. if it sees the use before the declare, it does not. The vast majority of these just go in some .h file and work without a second copy, but from time to time, you need that 2nd or even 3rd copy to make it go.

Additional declares can sometimes shortcut a weird, deep include chain, as another use beyond circular. Say you had some giant thing like boost, and needed some really simple function from it, and to get that you had to add some 2 dozen more files to your project because this requires that which requires the other ... none of it anything you used, but needed due to the source code tag-alongs. Or, you can declare the 2 or 3 things you need again and bypass all that, cutting it down to 2-3 files that are strictly necessary instead.
Last edited on
Compiling a C or C++ program into a stand-alone executable fit for the OS requires several discrete steps. There are 4 basic steps taken:

1. Pre-processing
2. Compilation
3. Assembly
4. Linking

https://www.geeksforgeeks.org/compiling-a-c-program-behind-the-scenes/

More steps may be done at the discretion of the compiler implementation.

C++ compilation is a bit more complicated due to the complexity of the language compared to C. Templates, classes, etc. The C++ Standard defines "9 phases of translation," though an implementation can run the process however it wants as long as the end result is what the standard mandates.

https://stackoverflow.com/questions/8833524/what-are-the-stages-of-compilation-of-a-c-program

For reference compiling C code goes through a similar number of phases, omitting the phase(s) for C++ language features. Templates, for instance.

Adding modules to the mix changes what can happen during the compiling process. Visual Studio, currently the only compiler that implements modules, reports what appears to be a new step in the early phases of the compiling process, scanning for module dependencies.

End result should make one realize there is a lot more happening when compiling code than appears to be going on. Remember that the next time building code seems to be "taking a long time." :)
Last edited on
When you declare a class as forward, you have created what the compiler calls an "incomplete type". There are only certain things you can do with an incomplete type. Those are to use it as a pointer or a reference. This is fine because the compiler knows how big a pointer is, even if it doesn't know what it points to looks like.
1
2
  B *    ptr;     // Okay, simple pointer
  B      b;       // Error, complier does know yet what B looks like yet 

Once B has been declared, then the second line would be okay.
the solution is supposed to come in form of forward declarations.


Not quite. You can forward declare classes. This means that the compiler knows the specified name is a class, but knows nothing else about that class.

Hence if you use that class name before it is defined then you are strictly limited into how that class name is used. You can only use it in terms of either a pointer or a reference. You can't say define a variable of that class.

1
2
3
4
5
6
struct A;

struct B {
    // A a;    // This is not allowed as nothing is known yet about A
    A* aptr;   // This is OK as aptr is a pointer to A.
};

Last edited on
Hi guys,

thank you very much for your many responses. So there is nothing wrong with re-declaration.

@George P
Thanks for the explanation and the links. It sure helps to know roughly what the compiler does.

@seeplus
I understand... so this is the reason why in the examples I posted (copied from a book), the implementation of class A can only use a reference to class B (or a pointer), but not an instance of class B itself.
> You can only use it in terms of either a pointer or a reference.

We can use the incomplete type in declarations of variables and functions, and in the instantiation of carefully written class templates.

For example:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#include <iostream>
#include <vector>

struct A ;

extern A aa ; // declaration; fine even if A is an incomplete type
A foo( A arg ) ; // declaration; fine even if A is an incomplete type

// assert: incomplete type support for std::vector, std::list and std::forward_list
// ie. the container (only the container, not its members) can be instantiated with an incomplete value_type
// provided its allocator satisfies the allocator completeness requirements.
static_assert( __cpp_lib_incomplete_container_elements == 201505L ) ; // C++17

struct B 
{ 
    std::vector<A> vec ; // instantiate std::vector<A> (though A is an incomplete type here)
    static A sa ; // declaration; fine even if A is an incomplete type
    B() ;
}; 

int main()
{
    std::cout << sizeof(B::vec) << '\n' ;
}

// TO DO: first make A a complete type, and then define aa, foo(), B::sa and B::B() 

https://coliru.stacked-crooked.com/a/d2c3db727ea611a5
> I understand... so this is the reason why in the examples I posted (copied
> from a book), the implementation of class A can only use a reference to
> class B (or a pointer), but not an instance of class B itself.

This is definition of B:
1
2
3
4
struct B {
    A* aptr;
    B(A*);
};

Q: How much memory will we need for object of type B?
A: For one pointer (and perhaps padding)

The answer is based on the definition and does not need to know how much memory objects of type A do require.


This is implementation (of one member) of B:
1
2
3
4
5
B::B(A* a)
 : aptr{a}
{
  if (aptr) aptr->hello();
}

This has to know the definition of A, or else it could not verify that A::hello() does exist.


For extra fun: https://en.cppreference.com/w/cpp/language/pimpl
This, and a lot of other design issues, are discussed in this great book:

https://www.amazon.co.uk/gp/product/0123850037/ref=ppx_yo_dt_b_search_asin_title
Topic archived. No new replies allowed.