I have a very simple CRTP skeleton structure that contains just one vector and a private accessor in the base class. There is a helper method in the CRTP class to access it.
#include <vector>
template<typename T>
class CRTPType {
// Will be used by other member functions. Note it's private,
// so declval/decltype expressions outside of friends or this class
// cannot see it
auto& _v() {
return static_cast<T *>(this)->getV();
}
};
class BaseType : public CRTPType<BaseType> {
friend class CRTPType<BaseType>;
std::vector<int> v;
//For different base types this impl will vary
auto& getV() { return v; }
};
So far so good. Now I want to add a using
declaration to CRTPType
that will be the type that _v()
returns. So ideally, one could do something like the following:
template<typename T>
class CRTPType {
//Will be used by other member functions
auto& _v() {
return static_cast<T *>(this)->getV();
}
using vtype = decltype(std::declval<CRTPType<T>>()._v());
};
The problem is that the type is incomplete, so I cannot use any decltype
/declval
expressions within CRTPType
.
Is there a clean way around this that breaks encapsulation as little as possible? Ideally in C 14, but I would be interested if there are any newer language features that help.
CodePudding user response:
If you don't care so much where and how exactly the using
declaration appears, then you can avoid the issue without much changes for example by putting the using
declaration into a nested class:
template<typename T>
class CRTPType {
//Will be used by other member functions
auto& _v() {
return static_cast<T *>(this)->getV();
}
struct nested {
using vtype = decltype(std::declval<CRTPType<T>>()._v());
};
};
The issue in your original code is that using
declarations are implicitly instantiated with the class template specialization and the point of implicit instantiation of CRTPType<BaseType>
is before the definition of BaseType
because the latter uses the former as a base class (which are required to be complete and are therefore implicitly instantiated).
Nested member classes are on the other hand specified not to be implicitly instantiated with the class template specialization and instead only when they are required to be complete. In other words the point of instantiation of the using
declaration will now be immediately before the namespace scope declaration which actually, directly or indirectly, uses nested::vtype
.
Another alternative is to make the using
declaration a template:
template<typename T>
class CRTPType {
//Will be used by other member functions
auto& _v() {
return static_cast<T *>(this)->getV();
}
template<typename U = T>
using vtype = decltype(std::declval<CRTPType<U>>()._v());
};
Member templates can not be implicitly instantiated with the containing class template specialization either. It may be necessary to use the U = T
construction and only use U
in using =
. The reason is that if the type on the right-hand side is not dependent on a template parameter, the compiler is allowed to check whether it can be instantiated immediately after the definition of the template, which is exactly what is not possible and we want to avoid. The program may therefore be ill-formed, no diagnostic required if only T
is used. (I am really not 100% sure this applies here, but Clang does actually complain.)
Another possibility is to move the using
declaration outside the class, in which case it is obvious that it will not be implicitly instantiated with it:
template<typename T>
class CRTPType;
template<typename T>
using CRTPType_vtype = decltype(std::declval<CRTPType<T>>()._v());
template<typename T>
class CRTPType {
//Will be used by other member functions
auto& _v() {
return static_cast<T *>(this)->getV();
}
};
Variations using nested classes or namespaces coupled with using
declarations in the enclosing namespace can hide the additional name from the outside.
With all of the above you still need to be careful that nothing else in the instantiation of CRTPType<BaseType>
or in the definition of BaseType
actually indirectly uses vtype
. If that happens you may be back to the original problem, potentially even depending on the declaration order of members (although that technically is not standard-conform behavior of the compilers).
In any case, you need to friend
the CRTPType<BaseType>
in BaseType
or mark getV
in BaseType
as public
.