I have a template class I would like to specialize for many different types. Something like:
#include <type_traits>
#include <string>
#include <string_view>
// To allow static assert for invalid branches
template <typename T>
inline constexpr bool invalid = false;
template <typename T>
struct TypeMeta
{
using type = T;
using intermediate_type = T;
static consteval auto marshal_string() {static_assert(invalid<T>, "Not implemented for type.");}
static constexpr auto convert(intermediate_type &&value) {return type{value};}
static constexpr bool needs_conversion = !std::is_same_v<type, intermediate_type>;
};
I would like to be able to override parts of this class like so:
struct TypeMeta<std::string>
{
using intermediate_type = const char *;
static consteval std::string_view marshal_string() {return "s";}
};
Then use this further for something like:
template <typename T>
T extract_from(void *data)
{
using meta_t = TypeMeta<T>;
if constexpr (meta_t::needs_conversion) {
typename meta_t::intermediate_type tmp;
tmp = *(static_cast<meta_t::intermediate_type *>(data));
return meta_t::convert(tmp);
} else {
return *(static_cast<meta_t::type *>(data));
}
}
Obviously, this doesn't work for std::string
because I haven't redefined type
, needs_conversion
, and convert
in my specialization.
Now, I can get around this pretty easily by having a base class to inherit from using CRTP. Something like:
template <typename T>
struct TypeMeta;
template <typename T>
struct TypeMetaBase
{
using type = T;
using intermediate_type = T;
static consteval auto marshal_string() {static_assert(invalid<T>, "Not implemented for type.");}
static constexpr type convert(intermediate_type &&value) {return type{value};}
static constexpr bool needs_conversion = !std::is_same_v<typename TypeMeta<T>::type, typename TypeMeta<T>::intermediate_type>;
};
template <typename T>
struct TypeMeta : public TypeMetaBase<T>{};
template <>
struct TypeMeta<std::string> : public TypeMetaBase<std::string>
{
using intermediate_type = const char *;
static consteval std::string_view marshal_string() {return "s";}
};
Which works just fine. I think it's slightly bad that you have to remember to inherit from the base for every type, but that's probably unavoidable.
The real problem, however, comes when you want to specialize just the one or two functions you actually need to from the parent. I want to do this:
template <>
consteval std::string_view TypeMeta<int>::marshal_string() {return "i"};
But this is not allowed:
<source>:xx:yy: error: no member function 'marshal_string' declared in 'TypeMeta<int>'
xx | consteval auto TypeMeta<int>::marshal_string() {return "i";}
I can get around this by duplicating the method in my template:
template <typename T>
struct TypeMeta : public TypeMetaBase<T>
{
static consteval auto marshal_string() {static_assert(invalid<T>, "Not implemented for type.");}
};
Which means that specialized structs like the std::string
one will fall back to using TypeMetaBase
's method if it's not defined, while structs that only have some specialized methods like the int
one will use TypeMeta
's method for ones that aren't specialized.
This means I have to duplicate the code unnecessarily, however. I don't like that. It can easily get out of sync, and it's unnecessary bloat to boot.
Is there any way to avoid having to duplicate methods while allowing minimal extra typing? I suppose I could just demand that any specialization declare its own struct, but there are many different types, most of which only require a single overridden method, and I would prefer to keep it as concise as possible.
One extra idea might be to use a macro for extra struct declarations to avoid the boilerplate, but I would prefer to avoid using macros if possible.
I basically just want to get a nice C++ solution which doesn't involve abusing the preprocessor or copy-pasting code.
You might split each component in its own type_traits:
// All those are customization point
template <typename T>
struct intermediate_type { using type = T; };
template <typename T>
using intermediate_type_t = typename intermediate_type<T>::type;
template <typename T>
consteval std::string_view marshal_string() = delete; // Has to be implemented
template <typename T>
constexpr auto convert(intermediate_type_t<T> value) {return T{value};}
template <typename T>
constexpr bool needs_conversion = !std::is_same_v<T, intermediate_type_t<T>>;
// You might keep the one to group them
template <typename T>
struct TypeMeta
{
using type = T;
using intermediate_type = intermediate_type_t<T>;
static consteval auto marshal_string() { return ::marshal_string<T>();}
static constexpr auto convert(intermediate_type value) {return ::convert<T>(value);}
static constexpr bool needs_conversion = ::needs_conversion<T>;
};
And then just provide specialization of individual traits:
template <>
struct intermediate_type<std::string>
{
using type = const char *;
};
template <>
consteval std::string_view marshal_string<std::string>() { return "s"; }
template <>
consteval std::string_view marshal_string<int>() { return "i"; }