Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Curiously mutually recurring class definitions

Tags:

I want type declarations in two classes to mutually depend on each other. Here is a first example that compiles both with clang and gcc:

template <class Sum>
struct A
{
    using X = char;                // (1)
    using Z = typename Sum::B::Y;  // (2)
};

template <class Sum>
struct B
{
    using Y = typename Sum::A::X;
};

struct AplusB
{
    using A = ::A<AplusB>;
    using B = ::B<AplusB>;
};

AplusB::A::Z z;

int main() {}

There is an interesting moment, however. If you swap lines (1) and (2), then it will fail to compile with an error:

error: no type named 'X' in 'A'

That makes me question whether the original code is actually valid in the sense of the C++ standard, or it just so happens to compile?

Here is a second example, which also exploits order of template instantiation:

template <class Sum>
struct A
{
    using X = char;
    using P = typename Sum::B::Q;
};

template <class Sum>
struct B
{
    using Y = typename Sum::A::X;
    using Q = int;
};

struct AplusB
{
    using A = ::A<AplusB>;
    using B = ::B<AplusB>;
};

AplusB::A::X z; // (1)
AplusB::B::Q t; // (2)

int main() {}

Here if you swap (1) and (2) it will fail to compile with error:

error: no type named 'Q' in 'B'

So the question is: Is it actually permitted by standard for class definitions to depend on each other like that?

like image 388
D. Dmitriy Avatar asked Jan 12 '18 13:01

D. Dmitriy


1 Answers

As discussed in another answer, CWG 287's resolution is just the de facto approach followed by implementations, mandating that precisely the entities that precede the "inline" PoI of the member being instantiated are in scope.

Thus, when the lines are swapped, we attempt to access something that hasn't been instantiated yet:

template <class Sum>
struct A
{
    using X = char;               // (#)
    using P = typename Sum::B::Q; // (2.3), (1.2)
};

template <class Sum>
struct B
{
    using Y = typename Sum::A::X; // (2.2), (1.3)
    using Q = int;                // (*)
};

struct AplusB
{
    using A = ::A<AplusB>; // (1.1)
    using B = ::B<AplusB>; // (2.1) 
};

AplusB::B::Q t; // (2)

AplusB::A::X z; // (1) (INDEPENDENT example)

The sequence (2), (2.1), (2.2) and (2.3) is the order in which the instantiations are caused, with the effective PoI preceding that declaration. With (2) declared first, at (2.3), we reference (*), which isn't in scope. If (1) comes first, then at (1.3) we access (#), which is indeed in scope. (In a subsequent declaration of (2), the specialisation of A has already been instantiated fully, so there are no further subtleties.)

If you declare all the "base case" aliases before the "recursive" ones, which was the difference in your first snippet, it works fine either way.

like image 176
Columbo Avatar answered Nov 11 '22 15:11

Columbo