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:
true
). By indirectly I mean they'll get a bunch of compilation errors that one can only solve if they know the answer beforehand.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) ...
an answer to any of the above is appreciated
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.
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).
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);
}