Brice M. Dempsey
Brice M. Dempsey

Reputation: 2161

How can I avoid circular dependencies in a header only library?

I am a developing a C++ header only library.

There are several parts of the code which follow this pattern:

holder.h

#pragma once

#include "sub.h"

struct Holder
{
    void f();
    Sub s;
};

sub.h

#pragma once

struct Holder;

struct Sub
{
   void g(Holder& h);
};

#include "sub.ipp"

sub.ipp

#include "holder.h"

inline void Sub::g(Holder& h)
{
    h.f();
}

sub.h avoids the circular dependency on Holder using forward declaration. However, in holder.h as the Holder class contains a Sub member it needs to see the full declaration of Sub in sub.h. sub.h, however, pulls in the implementation in sub.ipp which can't be instantiated yet as it needs the definition of Holder and we are already inside holder.h so we can't include it again.

As I user of any of these headers I would like to only have to include the correct .h file and not have to worry about manually including the correct .ipp files in strange places.

What is the standard solution to this?

Upvotes: 2

Views: 679

Answers (2)

ZeroZ30o
ZeroZ30o

Reputation: 396

I've spent some time figuring this out, here is a technique expanded from @eerorika 's answer.

There are two problematic cases:

  • Case 1: A contains B, B uses A
  • Case 2: A uses B, B uses A

If your structs don't fall under any of these cases, you can type out both the declaration and the implementation in the same header file.

For both case 1 and 2, my technique is to split the file into .hpp, .hppdecl and .hppimpl.

Note:

  • Storage of B* (pointer) counts as "uses" and not "contains";
  • Inheritance counts as "contains";
  • Templating can be "uses", "contains", or neither if it is only used for type safety;

A.hpp:

  • Includes A.hppdecl first.
  • Includes A.hppimpl second.

A.hppdecl: (contains declarations)

  • If A contains B (case 1), include the B.hppdecl.
  • If A uses B (case 2), forward declare.

A.hppimpl: (contains implementations)

  • Includes A.hppdecl first.
  • If A contains B (case 1), include the B.hppimpl.
  • If A uses B (case 2), include the B.hppdecl and the B.hppimpl.

And, do the same for B's files.


Why? Because you must respect the following restrictions/guidelines:

  • If A contains B and B uses A (case 1), B's declaration and implementation must be split (because Adecl must include Bdecl, and Bimpl must include Adecl)
  • If a declaration is included, its corresponding implementation must be guaranteed to be included at some point
  • Declarations must precede their own implementation, and can precede every other implementation

Upvotes: 1

eerorika
eerorika

Reputation: 238311

struct Sub
{
   void g(Holder& h);
};

void Sub::g(Holder& h)
{
    h.f();
}

Non-inline functions won't work well in header-only libraries, because the headers are typically included into more than one translation unit. You should use inline functions instead.


How can I avoid circular dependencies in a header only library?

You'll have to separate the definition of the functions from the definition of the class. I mean, they are in separate files already, but the header defining the class cannot include the function definitions. That allows breaking the dependency cycle.

This may be a matter of taste, but I also dislike "ipp" headers that don't work standalone.

Example:

detail/holder_class_only.h

#pragma once
#include "detail/sub_class_only.h"
struct Holder
{
    inline void f(); // note inline
    Sub s;
};

detail/sub_class_only.h

#pragma once
struct Holder;
struct Sub
{
   inline void g(Holder& h); // note inline
};

detail/holder_functions.h

#pragma once
#include "detail/holder_class_only.h"
void Holder::f()
{
}
#include "detail/sub_functions.h"

detail/sub_functions.h

#pragma once
#include "detail/sub_class_only.h"
#include "holder.h"
void Sub::g(Holder& h)
{
    h.f();
}

sub.h

#pragma once
#include "detail/sub_class_only.h"
#include "detail/sub_functions.h"

holder.h

#pragma once
#include "detail/holder_class_only.h"
#include "detail/holder_functions.h"

Note: Unstested, may contain bugs.

Upvotes: 5

Related Questions