#circular dependency issue with friend declarations

138 messages · Page 1 of 1 (latest)

wind harness
#
// A.h
#pragma once

template <typename Derived, typename T>
struct IStuff;

template <typename Base>
struct StuffCommon;

struct StuffImpl;

struct A {
private:
    int i = 12345;

public:
    friend struct StuffCommon<IStuff<StuffImpl, A>>;
};```
```cpp
// Stuff.h
#pragma once

struct A;

template <typename Derived, typename T>
struct IStuff {
    using value_type = T;

    T data;
};

template <typename Base>
struct StuffCommon : Base {
    void foo();
};

struct StuffImpl : StuffCommon<IStuff<StuffImpl, A>> {};```
```cpp
// Stuff.cpp
template <typename Base>
void StuffCommon<Base>::foo() {
    std::cout << this->data.i << '\n'; // private member access
}

// explicit instantiation
template struct StuffCommon<IStuff<StuffImpl, A>>;```
modern radishBOT
#

When your question is answered use !solved to mark the question as resolved.

Remember to ask specific questions, provide necessary details, and reduce your question to its simplest form. For tips on how to ask a good question use !howto ask.

wind harness
#

when I try to compile this code I get the following error

#

im not sure how to reformulate my code so that the incomplete type issue goes away

bright tartan
#

In your Stuff.h in the last line:

struct StuffImpl : StuffCommon<IStuff<StuffImpl, A>> {};

You never defined A, you just declared it.
At least that'd be my initial guess

analog trellis
#

the issue is that A's full definition is needed to make it a member of IStuff

bright tartan
#

Maybe you forgot to #include "A.h" in your Stuff.h?

analog trellis
#

A's full definition needs to be visible by the point StuffImpl inherits from a base that needs A to be a complete type

analog trellis
#

if IStuff shouldn't know about A but StuffImpl needs to, then they need to reside in different files

wind harness
#

in practice IStuff is its own header

bright tartan
wind harness
#

i should have separated it in my example ill edit it

analog trellis
bright tartan
wind harness
#
// A.h
#pragma once

template <typename Derived, typename T>
struct IStuff;

template <typename Base>
struct StuffCommon;

struct StuffImpl;

struct A {
private:
    int i = 12345;

public:
    friend struct StuffCommon<IStuff<StuffImpl, A>>;
};```
```cpp
// IStuff.h
#pragma once

template <typename Derived, typename T>
struct IStuff {
    T data;
};```
```cpp
// Stuff.h
#pragma once

#include "IStuff.h"

struct A;

template <typename Base>
struct StuffCommon : Base {
    void foo();
};

struct StuffImpl : StuffCommon<IStuff<StuffImpl, A>> {};```
```cpp
// Stuff.cpp
template <typename Base>
void StuffCommon<Base>::foo() {
    std::cout << this->data.i << '\n'; // private member access
}

// explicit instantiation
template struct StuffCommon<IStuff<StuffImpl, A>>;```
analog trellis
#

you went through the hassle of forward-declaring a bunch of things in A.h to avoid including Stuff.h there, if that's not to include A.h in Stuff.h then I don't know what it is for 😛

wind harness
#

ok my example was not 1:1 with my actual code in practice because i just realized I had A included in StuffImpl.h in my actual codebase but the error persists

#

i need some time to figure dis out

#

i think its an issue with type constraints...'

#
// A.h
#pragma once

#include "Concept.h"

template <typename Derived, typename T>
struct IStuff;

template <IsStuff_A Base>
struct StuffCommon;

struct StuffImpl;

struct A {
private:
    int i = 12345;

public:
    friend struct StuffCommon<IStuff<StuffImpl, A>>;
};```
```cpp
// IStuff.h
#pragma once

template <typename Derived, typename T>
struct IStuff {
    using value_type = T;

    T data;
};```
```cpp
// StuffImpl.h
#pragma once

#include "IStuff.h"
#include "Concept.h"
#include "A.h" // StuffImpl can see full definition of A

template <IsStuff_A Base>
struct StuffCommon : Base {
    void foo();
};

struct StuffImpl : StuffCommon<IStuff<StuffImpl, A>> {};```
```cpp
// StuffImpl.cpp
template <IsStuff_A Base>
void StuffCommon<Base>::foo() {
    std::cout << this->data.i << '\n'; // private member access
}

// explicit instantiation
template struct StuffCommon<IStuff<StuffImpl, A>>;```
```cpp
// Concept.h
#pragma once

#include <concepts>

struct A;

template <typename T>
concept IsStuff_A = std::same_as<typename T::value_type, A>;```
#

introducing the IsStuff_A yields the error i was experiencing in my real codebase

#

if it was possible to define the friend classes out of line that would be nice...

wind harness
#

bump :))

mighty iron
#

Jesus what a mess. Good luck!

analog trellis
#

can this be considered for Concept.h?

#pragma once

#include <concepts>

template <typename T, typename U>
concept IsStuff = std::same_as<typename T::value_type, U>;

usage:

template<IsStuff<A> Base>
struct StuffCommon : Base {
    // ...
}

template<IsStuff<A> Base>
void StuffCommon<Base>::foo() {
    // ...
}
#

the point in doing this is that hopefully by the time IsStuff is evaluated, along with instantiations that require A to be complete, A will be complete

wind harness
#

and in fact, my actual codebase essentially was doing the same thing

#

where teh concept required you to input the "stuff" type

#
// A.h
#include "Concept.h"

struct A;

template <typename Derived, typename T>
struct IStuff;

template <IsStuff<A> Base>
struct StuffCommon;

struct StuffImpl;

struct A {
private:
    int i = 12345;

public:
    friend struct StuffCommon<IStuff<StuffImpl, A>>;
};```
```cpp
// IStuff.h
template <typename Derived, typename T>
struct IStuff {
    using derived_type = Derived;
    using value_type = T;

    T data;
};```
```cpp
// Stuff.h
#include "IStuff.h"
#include "Concept.h"
#include "A.h" // StuffImpl can see full definition of A

template <IsStuff<A> Base>
struct StuffCommon : Base {
    void foo();
};

struct StuffImpl : StuffCommon<IStuff<StuffImpl, A>> {};```
```cpp
// Stuff.cpp
template <IsStuff<A> Base>
void StuffCommon<Base>::foo() {
    std::cout << this->data.i << '\n'; // private member access
}

// explicit instantiation
template struct StuffCommon<IStuff<StuffImpl, A>>;```
```cpp
// Concept.h
#include <concepts>

template <typename Derived, typename T>
struct IStuff;

template <typename T, typename U>
concept IsStuff = (std::derived_from<T, IStuff<typename T::derived_type, typename T::value_type>> && std::same_as<typename T::value_type, U>);```
#

here I applied the generalized concept (and an extra check to see if its actually "stuff" but thats just so the example looks a bit more practical)

#

What I did consider is removing the IsStuff concept as part of the StuffCommon type signature and making a static_assert inside the class body (something like static_assert(IsStuff<Base, A>). So in essence, its like a lazy type constraint and this would avoid me needing to have A fully defined to declare friend struct StuffCommon<IStuff<StuffImpl, A>>; within A. But im not sure I like this approach because its less readable and might have some unintended consequences but idk. Its also unintuitive why one would write it this way

analog trellis
#

It's kinda unintuitive and more importantly it would allow "invalid" specializations to be selected and instantiated, which will result in a compile error rather than a substitution/subsumption failure

#

the kind of problems that isn't a problem until it is one, and then it's unsolvable

analog trellis
analog trellis
#

so it seems the friend declaration in A causes StuffCommon<IStuff<StuffImpl, A>> to be instantiated and that's where problems are stemming from - I get why you titled this "circular dependency" now, I didn't know friend declarations caused instantiations

#

I don't know how you can easily solve this

#

does this make things better:

template<typename T>
friend struct StuffCommon;
#

(hoping you don't get something about declaration less constrained than previous declaration)

wind harness
#

assuming this is what u were suggesting

#

making it a template template parameter doesnt work either

analog trellis
#

remove <T> from it

wind harness
#

ok my brain didnt register that

#

(<T> is removed the tooltip just blocks it)

analog trellis
#

well I'm out of ideas then, sorry 😦

wind harness
#

thank u

analog trellis
#

ok last thing to try out:

template<typename T>
template<IsStuff<T> Base>
friend struct StuffCommon;
#

I'm guessing that counts as a partial specialization declaration though 🫠

wind harness
#

but isnt this saying something different tho

#

I only want StuffCommon with A to be a friend

#

but any stuffcommon

analog trellis
#

having a regular friend declaration didn't work because you would run into the first errors that had in this post, so yeah we're trying out template friend declarations

#

which are broader sure but hey if they work... ¯_(ツ)_/¯

analog trellis
#

templates of template declarations are thing yeah

#

well sorry for wasting your time so far anyway haha

wind harness
#

nah i mean ive been stuck on it for 2 days its 0.01% of time spent, not wasted. lots of new ideas

formal rose
analog trellis
#

maybe catch an unsuspecting expert in #cpp-help-text

#

oh well, there's one yamikek

formal rose
#

what does having the constraint in the template achieve

#

gonna assume that the general structure makes sense

#

and that semantically you only want base types in stuff common that satisfy IsStuff<A>

#

why does that have to strictly be in the template definition

#

sure you advertise immediately and upfront that you want the constraint to be satisfied, but do you need this, or do you just want it

wind harness
#

I want it. i feel like thats half the reason we use concepts if not for substitution or subsumption

formal rose
#

in your original design, there are no concerns about picking a wrong specialization

analog trellis
#

that is true yeah

formal rose
#

so having the constraint in the template declaration directly achieves little from a practical point of view

formal rose
wind harness
formal rose
#

if you have

template <IsThing T>
struct foo;

then there are no concerns about picking the right specialization based on whether IsThing will be satisfied

#

because regardless of what specialization you try to introduce T always has to satisfy IsThing no matter what

wind harness
wind harness
wind harness
#

its hard to explain all the background details without frontloading a bunch of paragraphs of context 🫠 (surely no one would want to attempt to answer that)

formal rose
#

I get the general sentiment of wanting the constraint somewhere, but the way you're defining them and imposing them is too recursive, so I still raise the question of what it achieves to have the constraint then and there

#

is IntObjectCommon supposed to be exposed to the end user

#

and is he allowed to specialize it

wind harness
#

only IntObjectImpl is front facing

formal rose
#

if he isn't, and only the primary template is ever intended to be used, then to what extent does it hurt to put a static assert in the class body

#

rather than put an actual constraint directly on the template class definition

#

actually I guess I should check if the static assert is triggered in the relevant contexts

wind harness
#

does not just putting it anywhere in the class body suffice

#

what other relevant contexts could there be

wind harness
#

thats what i was wondering

formal rose
#

joyous things with what is/isn't immediate context

#

a static_assert in the class body probably will not trigger if you do something like using some_alias = StuffCommon<invalid_type>;

#

but for an internal implementation detail that isn't user facing, and specifically a type for which you want to have concrete instances of, that often isn't a true problem

#

it's just non-nice

wind harness
#

yea i can see what you mean

wind harness
formal rose
#

though it's often "nicer" than putting a static assert in every single member function

wind harness
#

would there be a potential functional difference between putting a static assert in c lass body and one in every member function

formal rose
formal rose
#

crtp + concepts generally do not mesh together well, unless you break the recursion somewhere in some way

#

many concepts will typically require the type to be fully defined

#

which makes them not suitable for use in most crtp contexts

formal rose
#

if a member function is never used, then whatever is in its body doesn't matter

#

that's similar to how you can use a map with non-default-constructible mapped types, if you don't use []

wind harness
#

i meant to say, if i already have the static assert at the start of the class in the body, would adding the same assert anywhere else in the class (i.e. a member function) have any functional difference

formal rose
formal rose
#

but honestly this is overall too obscure a thing for me to assert anything

#

when you start going into that, there's too much I haven't actually read up on

wind harness
#

okie

#

well i think ill just do away with some concepts in my implementation details

modern radishBOT
#

@wind harness Has your question been resolved? If so, type !solved :)

modern radishBOT
#

Thank you and let us know if you have any more questions!

This thread is now set to auto-hide after an hour of inactivity

formal rose
#

which is overall simpler to manage imo, for things where you don't critically need the constraint in the class template declaration

#

one possible way to break the recursion, though obviously only in the reduced example of the thread (so you'll be on your own for actual thing) https://godbolt.org/z/57eYzq6TW

#

to an extent it has "nicer" behaviour but I can't bring myself to recommend it

#

if the thing was user facing then actually maybe do it just to have an additional layer to prevent the user from messing up

#

but to an extent it feels like you add more complexity for something fairly brittle

#

(and if you were wondering, I was waiting for a compilation and got bored)

wind harness
#

i did end up going with static asserts in class body

wind harness