0

I have two template classes which both depend on each other. As an example consider

template< typename > struct B;

template< typename T > struct A {
    void doSomething() {
        B<T> b{t};

        b->doSomething();

        // do some more
    }

    T t;
};

template< typename T > struct B {
    void doSomething() {
        // do something
    }

    A<T> createA() {
        // Something
    }
};

Based on my understanding of this, two-phase-lookup renders my snippet above ill-formed, because A's implementation requires type B to be complete, but at the time A is implemented here, B is still incomplete.

If these were not template classes, the problem would vanish as we would only require B in A.cpp. Therefore, when B.h includes A.h (because B should actually require A to be complete at the time it declares createA).

However, since these are templates, we have to define everything in header files, which seems to makes this a lot more complicated.

How are such situations typically resolved?

Raven
  • 2,951
  • 2
  • 26
  • 42
  • 4
    also methods of class templates can be defined seperate from their declaration, the issue then vanishes – 463035818_is_not_an_ai Jan 20 '23 at 10:46
  • A common software design guideline is to avoid strong coupling. Is this the only possible solution to your [actual](https://meta.stackexchange.com/questions/66377/what-is-the-xy-problem) problem? – Bob__ Jan 20 '23 at 10:46
  • @Bob__ in my case the actual situation is as follows: I have a data structure `S` for which I implement an iterator `I` that is supposed to return a Wrapper `W` instead of the actually stored data type inside `S` because the latter has a poor interface. The wrapper however, is required to be able to call functions of `S` in order to provide the better API. I'm not sure how I would go about decoupling this... – Raven Jan 20 '23 at 10:51
  • @463035818_is_not_a_number this would then require all template definitions to reside in the same header file, doesn't it? I usually try to keep every type in its own header file in order to make it easy to figure out in which header a given type is defined/declared... – Raven Jan 20 '23 at 10:53
  • 1
    Running the shown code through the latest version of gcc, which does not have a reputation for ignoring ill-formed code, does not result in any errors. Can you provide more detail why you believe the shown code is ill-formed? – Sam Varshavchik Jan 20 '23 at 11:14
  • @SamVarshavchik yeah, unfortunately no compile that I tried reported an error or at least a warning here. The reason why I think my code is ill-formed is due to the standard's requirement of a two-phase lookup (see the linked question). Unfortunately, the standard defines such programs as ill-formed, but does explicitly not require a diagnostic to be emitted by compilers... – Raven Jan 20 '23 at 11:19

2 Answers2

2

Anything that depends on T is a dependent type, and as such is looked up during the second phase.

You only need to forward-declare A before B, then you can use A<T> safely.

Of course both A and B need to be fully defined at the time you actually instantiate them.

template< typename T > struct A;

template< typename T > struct B {
    void doSomething() {}

    A<T> createA() { return {t}; };   // parsing deferred until 2nd phase

    T t;
};

template< typename T > struct A {
    void doSomething() {}

    T t;
};

int main() {
    A<int> a{5};
    a.doSomething();
    B<int> b{12};
    A<int> c = b.createA();
}
rustyx
  • 80,671
  • 25
  • 200
  • 267
  • Doesn't this still have the problem that `A` (return value of `createA`) is incomplete at the time `B` is defined? And shouldn't `T` be already a dependent type, given that it is a template parameter? – Raven Jan 20 '23 at 11:32
  • 1
    Umm ok now I don't understand what the actual problem is that you're experiencing. If `A` is already a dependent type, how is it a problem for the first phase? – rustyx Jan 20 '23 at 11:37
  • Yikes - I totally missed the fact that `A` is in fact a dependent type and therefore only considered in phase two... In that case the "problem" I described isn't even a problem in the first place and my code was fine all along. I guess I became overly paranoid after learning that compilers don't (necessarily) emit any diagnostic for cases in which two-phase-lookup fails.... – Raven Jan 20 '23 at 11:46
  • In the case that `A` would not actually depend on `T` but some other parameter, your initial suggestion with using an identity template is still very useful though! (it'd have to be adapted so it takes two types, returning only the first, so that the second can be `T` - e.g. `identity< int, T >`. That should be enough to cause the entire type (`A< identity< int, T > >`) to become a dependent type, right? – Raven Jan 20 '23 at 11:48
  • 1
    Yes, usually when T is involved then only basic syntax parsing is possible. Still quite a bit can be parsed in the first phase, so a good enough approach with these things is to "ask the compiler". GCC and clang are pretty strict with this, and since it's not an "ill-formed NDR" kind of rule but a lookup rule, if the code compiles then it's usually fine. – rustyx Jan 20 '23 at 12:32
0

Thanks to @463035818_is_not_a_number's comment I think I found the solution for my problem. Instead of only providing A.hpp and B.hpp, we can also define A_fwd.hpp and B_fwd.hpp.

This allows us to also separate declaration and definition of the template's functions by first declaring the template classes in the respective _fwd.hpp files and then putting the actual definition into the regular *.hpp files (after having included the respective _fwd.hpp).

For my example this could look like this:

A_fwd.hpp

#pragma once

template< typename T > struct A {
    void doSomething();

    T t;
};

B_fwd.hpp

#pragma once

#include "A_fwd.hpp"

template< typename T > struct B {
    void doSomething();

    A<T> createA();

    T t;
};

A.hpp

#pragma once

#include "A_fwd.pp"
#include "B_fwd.hpp"

template< typename T > void A<T>::doSomething() {
    B<T> b{t};

    b.doSomething();

    // do some more
}

B.hpp

#pragma once

#include "B_fwd.hpp"
#include "A_fwd.hpp"

template< typename T > void B<T>::doSomething() {
    // something
}

template< typename T > A<T> B<T>::createA() {
    return { t };
}

Godbolt

While this increases the amount of required typing, it essentially gives us the same rules to play by as for non-template classes, which are implemented as separate declarations (header) and definition (cpp-file).

Raven
  • 2,951
  • 2
  • 26
  • 42
  • 1
    You miss forward declaration and include to make the headers include order non-dependent. And so include guards is important (a case where `#pragma once` doesn't do the job). – Jarod42 Jan 20 '23 at 13:43
  • @Jarod42 indeed - I (think I) have corrected it – Raven Jan 20 '23 at 16:42
  • Should be fine now :) (I used to do the *"main"* include differently and so I have issue with `#pragma once` you don't have :) ) – Jarod42 Jan 20 '23 at 16:48
  • @Jarod42 oh hold on - I didn't fully read your comment. I always thought that include guards and `#pragma once` are completely equivalent. Mind sharing how you did the "main" include that leads to the pragmas behaving differently? :) – Raven Jan 20 '23 at 16:54
  • My goal is that main header contains class definition directly; So I put `#include` at end of file (`#include "a.hpp"` at end of a_fwd.hpp)) after the `#endif`. Something like [that](https://onlinegdb.com/DQaNmo1-B) – Jarod42 Jan 20 '23 at 17:18
  • @Jarod42 ah, I see. Is there an advantage to be had by doing it like this or is it more for stylistic reasons? – Raven Jan 22 '23 at 16:58
  • Stylistic and habits. – Jarod42 Jan 22 '23 at 17:16