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:
- 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. - There's no way of "requiring" them to create the two needed methods, namely
write2Cache
andreadFromCache
, 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
.