2

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?

Brice M. Dempsey
  • 1,985
  • 20
  • 16
  • With include guards, you might put some code outside the guards. Not possible with `#pragma once`... – Jarod42 Jun 17 '21 at 13:33
  • The solution you've shown here is the one of the two I use. The other is assuming the two classes are really part of the same component because they go hand-in-hand, and therefore they belong together in the same header file. – Eljay Jun 17 '21 at 13:38
  • You may need to pass `Holder` as a (smart) pointer as opposed to a reference. See https://stackoverflow.com/questions/334856/are-there-benefits-of-passing-by-pointer-over-passing-by-reference-in-c – Den-Jason Jun 17 '21 at 13:41
  • 1
    @Den-Jason: That doesn't change dependencies concern. Don't have to bother with `nullptr` currently, contrary to pointers. – Jarod42 Jun 17 '21 at 14:09
  • including separate headers with method impls makes the most sense with templates. Safe to assume that `Sub` is actually a template class? – Mooing Duck Jun 17 '21 at 15:02

2 Answers2

4
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.

eerorika
  • 232,697
  • 12
  • 197
  • 326
  • 1
    `#include "detail/sub_functions.h"` seems unneeded *"holder_functions.h"* – Jarod42 Jun 17 '21 at 14:31
  • 2
    @Jarod42 `seems unneeded "holder_functions.h"` It's there just in case, because when `"detail/sub_class_only.h"` is included, its functions may be called which will result in ODR violation (NDR) if `"detail/sub_functions.h"` isn't included. The "class_only" headers are a bit fragile because of that, but I see no reason to have that fragility in "functions" headers. – eerorika Jun 17 '21 at 14:36
  • 1
    @Jarod42 `inline should go in definition.` Why? `No needs in declaration` On the other hand, no needs in definition, when it's in the declaration. – eerorika Jun 17 '21 at 14:40
  • 1
    The question makes a lot more sense if you assume `Sub` is a template class. Then extracting it's methods to a separate header is _virtually required_. – Mooing Duck Jun 17 '21 at 15:03
1

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
ZeroZ30o
  • 375
  • 2
  • 18