Search code examples
c++headertypedefusingcircular-dependency

how to split cyclic dependency with 'using' directive


Suppose classes A and B. Each can construct the other:

// A.hpp
#pragma once
class B;
class A {
  B foo();
};
// A.cpp
#include <A.hpp>
#include <B.hpp>
B foo(){ return B(); }
// B.hpp
#pragma once
class A;
class B {
  A foo();
};
// B.cpp
#include <B.hpp>
#include <A.hpp>
A foo(){ return A(); }

Let's introduce a new type:

// A_or_B.hpp
#pragma once

#include "A.hpp"
#include "B.hpp"
// or
class A;
class B;

using A_or_B = std::variant<A, B>;

Let's return A_or_B from A and B:

// A.hpp
#pragma once

#include "A_or_B.hpp" // woops!  the type for A_or_B is not resolved:
class A {
  A_or_B foo();
};

Justifications for the whoops:

  1. if we use an include in A_or_B.hpp then A_or_B will not be able to include A (since the header guard) - so it will be referencing a non-existant type.
  2. if we use a forward declaration then A_or_B will use uncomplete types and will not be usable in the linking stage

The cyclic problem doesn't occur between A and B because we split the declaration and implementation between hpp and cpp files. contrary to classes - typedefs or using directives can't be split.

what can be done to solve this?


Solution

  • You can define A_or_B as a typedef for std::variant<A, B> without defining A and B. Only to instantiate an object of type A_or_B do you need the definitions.

    // A.hpp
    #include "A_or_B_fwd.hpp"
    
    class A {
        A_or_B foo();
    };
    
    // B.hpp
    #include "A_or_B_fwd.hpp"
    
    class B {
        A_or_B foo();
    };
    
    // A_or_B_fwd.hpp
    #include <variant>
    
    class A;
    class B;
    
    using A_or_B = std::variant<A, B>;
    
    // A_or_B.hpp
    #include "A.hpp"
    #include "B.hpp"
    #include "A_or_B_fwd.hpp"
    
    // A.cpp
    #include "A.hpp"
    #include "A_or_B.hpp"
    
    A_or_B A::foo() { /* ... */ }
    
    // B.cpp analogously
    

    Using "-fwd.hpp"-style headers can be reasonable when forward declarations are more complex than class X; The standard library also does it with <iosfwd>.