Home > Back-end >  Enforce class template specializations to provide one or more methods
Enforce class template specializations to provide one or more methods

Time:04-07

I'm using a "traits" pattern where I have a base case expressed as a class template

template <class>
struct DoCache {
  constexpr static bool value = false;
};

and I expect users to specialize for their types:

template <>
struct DoCache<MyType> {
  constexpr static bool value = true;

  static void write2Cache(MyType const&) { /* implementation */ }
  static optional<MyType> readFromCache(string name) { /* implementation */ }
};

The typical use is to retrieve and use this as:

// Define a variable template
template <class T>
constexpr bool do_cache_v = DoCache<T>::value;

// Use the above trait in compile time branching:
if constexpr (do_cache_v<T>)
{
  write2Cache(arg);
}

There's two problems I have with this code:

  1. A user is only indirectly enforced to provide a "value" member when specializing, let alone making it the proper value (i.e. true). By indirectly I mean they'll get a bunch of compilation errors that one can only solve if they know the answer beforehand.
  2. There's no way of "requiring" them to create the two needed methods, namely write2Cache and readFromCache, let alone having (const) correct types.

In some code-bases I've seen the considerations above being tackled by defining a generator macro like:

#define CACHABLE(Type, Writer, Reader) ...
  • Is there a better way to it?
  • Can concepts be used to restrict the way a specialization looks?
  • Is there a C 17 compatible way?

an answer to any of the above is appreciated

CodePudding user response:

C 17: Curiously recurring template pattern

It seems like a suitable use case for CRTP:

template<typename T> 
struct DoCache {
    void write2Cache() {
        static_cast<T*>(this)->write2Cache();
    }
    // ...
};

template<typename T>
void write2Cache(DoCache<T>& t) {
    t.write2Cache();
}

struct MyType : DoCache<MyType>
{
    void write2Cache() { /* ... */ }
};

int main() {
    MyType mt{};
    write2Cache(mt);
}

Instead of requiring clients to specialize a library type over their own types, you require them to implementes their own types in-terms-of (static polymorphism) the contract/facade of the library type.


C 20: Concepts

With concepts you can skip polymorphism entirely:

template<typename T>
concept DoCachable = requires(T t) {
    t.write2Cache();
};

template<DoCachable T>
void write2Cache(T& t) {
    t.write2Cache();
}

struct MyType {
    void write2Cache() { /* ... */ }
};

struct MyBadType {};

int main() {
    MyType mt{};
    write2Cache(mt);

    MyBadType mbt{};
    write2Cache(mbt);  // error: ...
      // because 'MyBadType' does not satisfy 'DoCachable'
      // because 't.write2Cache()' would be invalid: no member named 'write2Cache' in 'MyBadType'
}

However again placing requirements on the definition site of client type (as opposed to specialization which can be done after the fact).


Trait-based conditional dispatch to write2Cache()?

But how is the trait do_cache_v exposed this way?

C 17 approach

  • Since the CRTP-based approach offers an "is-a"-relationsship via inheritance, you could simply implement a trait for "is-a DoCache<T>":

    #include <type_traits>
    
    template<typename>
    struct is_do_cacheable             : std::false_type {};
    
    template<typename T>
    struct is_do_cacheable<DoCache<T>> : std::true_type {};
    
    template<typename T>
    constexpr bool is_do_cacheable_v{is_do_cacheable<T>::value};
    
    // ... elsewhere
    if constexpr(is_do_cacheable_v<T>) {
        write2Cache(t);        
    }
    

C 20 approach

  • With concepts, the concept itself can be used as a trait:

    if constexpr(DoCachable<T>) {
        write2Cache(t);        
    }
    

CodePudding user response:

You can use a concept to sanity check specializations. Here you only need to provide the correct, by name & type, methods hence the ::value member in DoCache can be deprecated:

template <class T>
concept Cacheable = requires (T const& obj) { 
    { DoCache<T>::write2Cache(obj) } 
        -> std::same_as<void>;
    { DoCache<T>::readFromCache(std::string{}) } 
        -> std::same_as<std::optional<T>>; 
};

Usage is similar to the trait:

if constexpr (Cacheable<MyStruct>)

and enforces proper specialization of DoCache.

Demo

  • Related