Template specializations are sometimes used to detect the validity of expressions, such as whether a member type of a class exists. This is useful for implementing trait classes. When a user does not provide a trait type inside the class, a fallback trait can be used by detecting this case.
For example, in the current version of Usagi Engine, a System has to define the access masks of Component types in order to allow the Executive to schedule the executions of Systems according to the data dependencies. (TBH this hasn’t been implemented yet, so it might change in the future.)
A sample System is shown below:
struct SystemFireworksExplode { using WriteAccess = ComponentFilter< ComponentFireworks, ComponentSpark, ComponentPosition, ComponentPhysics, ComponentSprite >; using ReadAccess = ...; };
Naive Trait Class
Two trait classes are used to obtain the access of a System to a particular Component:
template <System GameSystem, Component C> struct ComponentReadAccess : std::bool_constant< GameSystem::ReadAccess::template HAS_COMPONENT<C> > {}; template <System GameSystem, Component C> struct ComponentWriteAccess : std::bool_constant< GameSystem::WriteAccess::template HAS_COMPONENT<C> > {};
However, these two classes do not take into account the cases when the access mask is not defined. Because the write access implies read access (if the System only has read access, a const reference will be returned when requesting the Component; if the System has write access, it will get a mutable reference. However, there is no such thing as a write-only reference in C++), often the System only has to define write accesses and does not have to define any read access. But the current trait class forces the existence of a read access mask. Otherwise, the code won’t compile.
A better solution would be a conditional trait class falling back to a default access mask (none access allowed) when the trait type is not defined by the user. This requires detecting the existence of a member of a class.
SFINAE-Based Trait Class
One approach is using an SFINAE technique involving using std::void_t
as described here. (For the working mechanism, see this Stack Overflow answer.) Basically, it exploits the matching rules of partial template specialization by using an extra template parameter defaulting to void
. A partial specialization matches for that signature exactly in the case where the member type is defined. Therefore, due to the mechanism of SFINAE, when the target member type exists, the specialization is matched. Otherwise, the primary template is used.
Inside the bracket of std::void_t
, if it is a member variable to be detected, it should be scoped name inside decltype
like decltype(T::value)
. If it is a member type, it does not need decltype
.
Using this technique to define the trait class would look like:
template < System GameSystem, Component C, typename = std::void_t<> > struct ComponentReadMaskBitPresent : std::false_type {}; template < System GameSystem, Component C > struct ComponentReadMaskBitPresent< GameSystem, C, std::void_t<typename GameSystem::ReadAccess> > : std::bool_constant< GameSystem::ReadAccess::template HAS_COMPONENT<C> > {};
Constraint-Based Trait Class
The SFINAE approach works, but without understanding the internals it might seem confusing. With the new constraints and concepts from C++20, the partial template specialization can be constrained so that it will only be selected when the constraint is satisfied. Therefore, we can use a constraint expression to check whether the member of a type exists in place of the void_t
technique.
template <System GameSystem, Component C> struct ComponentReadMaskBitPresent : std::false_type {}; template <typename T> concept HasReadAccessMask = requires (T) { T::ReadAccess; }; template <System GameSystem, Component C> requires HasReadAccessMask<GameSystem> struct ComponentReadMaskBitPresent< GameSystem, C > : std::bool_constant< GameSystem::ReadAccess::template HAS_COMPONENT<C> > {};