I have a nested class definition that I wanted to pass down as the parent class (class containing the nested class, not the class that's being inherited) template parameter. Since the template does not seem to be aware of the nested class's existance, I tried to pass it down as an incomplete type, only to read later that doing so is usually a bad idea and is only rarely permitted (such as in the case of shared_ptr
).
I know this can be very easily solved by simply declaring the nested class externally (which is what I do) but I am asking this because I want to learn if there is any way of achieving the same effect because I an fond of how they do not pollute the namespace how they are "associated" with the parent class definition without exposing anything.
Here's an example to perhaps make it more clear on what I am talking about:
#include <memory>
using namespace std;
template <class T> class shared_class {
protected:
shared_ptr<T> d = make_shared<T>();
T* container() { return d.get(); }
};
// A is the "parent" class.
class A : public shared_class<C> { // Compiler error: C is undefined.
// Similarly, A::C will complain that no C is in A.
class C { // Nested class
public:
int i = 0;
};
};
Is there another way of doing this with templates in a way that the entire definition of the nested class is contained entirely within the parent class definition?
CodePudding user response:
In short: no you can't do this. There are a few similar questions you can find on SO, but it all comes down to being able to forward-declare the nested class, which you can't do without a definition. See for example this question.
In other words, with great hand-waving, you might think you could do something like this:
class A;
class A::C; // Error: not real C
class A : public shared_class<A::C> { // Error: incomplete base class
class C {
public:
int i = 0;
};
};
But you can't. You need to separate the declaration of C
, and it needs a definition if it's going to be used as a base class. See also questions like this.
If it's just namespace encapsulation you most care about, you can always just use a dedicated namespace
, that's what they're for.
CodePudding user response:
There is no solution for your specific case with the requirements you gave, as far as I can tell.
Directly using C
as template argument can't work in any way, simply because at that point the compiler hasn't seen yet that C
is declared as a member class of A
and because there is no way to declare the nested class before that point.
It doesn't work in your specific case, but with some limitations, you could add an indirection to the type of the shared_class
template parameter via a traits template or directly via an alias declaration, which would allow you to delay the determination that T
is supposed to be C
until after the definition of A
and keep C
as nested class.
Unfortunately that doesn't work if you want to use the type T
of shared_class
in one of the declarations that would be instantiated with the class template specialization, here shared_ptr<T> d
and T* container()
. The latter could be saved by declaring the return type auto
, but the former can't.
The idea would be to let shared_class
use typename T::shared_class_type
everywhere instead of T
.
The class A
would be defined as
class A : public shared_class<A> {
public:
class C {
public:
int i = 0;
};
using shared_class_type = C;
};
Alternatively you would define a template<typename T> struct shared_class_traits;
, potentially with a default implementation containing using shared_class_type = typename T::shared_class_type;
to be potentially specialized for A
after its definition with a using shared_class_type = A::C;
member, so that shared_class
can use typename shared_class_traits<T>::shared_class_type
everywhere. This is more flexible than "reserving" a specific member name of all classes using shared_class_type
.
But as I explained above it will not work as long as shared_class
uses shared_class_type
at all in a context that is instantiated with the class template specialization. So inside a member function body or default member initializer would be fine, but in a data member or a member function type is not.