please, help me understand what I'm doing wrong.
I'm trying to make a serializable variable wrapper using RapidJSON. I want to provide user-defined conversion functions or use default literal conversion functions. Also, I want to write a cross-compiled solution (MSVS & MinGW).
When a UserType isn't a literal type, select on compile-time a user-defined toJson / fromJson
, otherwise use the defined constructor rapidjson::Value ()
and rapidjson::Value ().template Get <LiteralType>
functions.
I struggled when trying to apply SFINAE or concepts to custom UserType. But for literal types (int, double, etc) it works fine.
Problem: Attempt to define via SFINAE a struct with converter is resolved by UserType failed because of an internal rapidJSON template error that I want to omit via SFINAE:
error: 'Get' is not a member of 'rapidjson::internal::TypeHelper<rapidjson::GenericValue<rapidjson::UTF8<> >, CustomType1>'
32 | T Get() const { return internal::TypeHelper<ValueType, T>::Get(*this); }
| ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^~~~~~~
The rapidjson types system recreated for a minimal example
namespace rapidjson {
template <class T = int>
class UTF8 { };
namespace internal
{
template <typename ValueType, typename T>
struct TypeHelper {};
template<typename ValueType>
struct TypeHelper<ValueType, double> { static double Get(const ValueType& v) { return v.GetDouble(); } };
template<typename ValueType>
struct TypeHelper<ValueType, int> { static double Get(const ValueType& v) { return v.GetInt(); } };
}
template <typename BaseAllocator = void>
class MemoryPoolAllocator { };
template <typename Encoding, typename Allocator = MemoryPoolAllocator<> >
class GenericValue
{
typedef GenericValue<Encoding, Allocator> ValueType;
public:
explicit GenericValue() = default;
explicit GenericValue(double d) { }
explicit GenericValue(int i) { }
template <typename T>
T Get() const { return internal::TypeHelper<ValueType, T>::Get(*this); }
double GetDouble() const { return 333.3; }
int GetInt() const { return 333; }
};
//! GenericValue with UTF8 encoding
typedef GenericValue<UTF8<> > Value;
}
My wrapper part
namespace Serializable {
// Base template for literal types, no constrains
// Assume that user for his type will define the same as the static function at UserJsonConversions class
template<class UserType>
void fromJson(UserType& userValue, const rapidjson::Value& initializer)
{ userValue = initializer.template Get<UserType>(); }
template <class UserType>
using FromJsonFuncPtrType = void(*)(UserType&, const rapidjson::Value&);
// As I know, if while checking statement for validity the error will be thrown,
// the concept must fails and belongs to it declaration expression must be skipped by SFINAE
template <class UserType>
concept HasFromJsonCast = requires {
rapidjson::Value().Get<UserType>();
};
template <class UserType>
concept NoFromJsonCast = !HasFromJsonCast<UserType>;
// SFINAE helper,
// In the base template suppose we received a custom user type
template <class UserType, class ExternalConversions, class = void>
struct Distinct {
inline static constexpr const FromJsonFuncPtrType<UserType> fromJson = &ExternalConversions::fromJson;
};
template <class UserType, class ExternalConversions>
struct Distinct<UserType, ExternalConversions, std::void_t<std::enable_if<HasFromJsonCast<UserType>, void>>> {
inline static constexpr const FromJsonFuncPtrType<UserType> fromJson = &fromJson;
};
}
User program
// User-related types and conversions
struct CustomType1 { };
struct CustomType2 { };
struct UserJsonConversions
{
static rapidjson::Value fromJson(CustomType1& customValue, const rapidjson::Value& initializer)
{ return rapidjson::Value(); }
static rapidjson::Value fromJson(CustomType2& customValue, const rapidjson::Value& initializer)
{ return rapidjson::Value(); }
};
template<class UserType>
inline auto attempt()
{
rapidjson::Value json;
UserType customValue;
Serializable::Distinct<UserType, UserJsonConversions>::fromJson(customValue, json);
}
int main() {
// attempt<double>();
attempt<CustomType1>();
}
How to correctly use SFINAE in my case?
So I solved it. First of all, I changed main concept from
template <class UserType>
concept HasFromJsonCast = requires {
rapidjson::Value().Get<UserType>();
};
to
template <class UserType, class ExternalConverters>
concept HasFromJsonUserCast = requires (UserType& userValue) {
{ ExternalConverters::fromJson(userValue, rapidjson::Value()) } -> std::convertible_to<void>;
};
And the helper Distinct class will look like
template <class UserType, class ExternalJsonConverters>
class Distinct {
using FromJsonFuncPtrType = void(*)(UserType&, const rapidjson::Value&);
using ToJsonFuncPtrType = rapidjson::Value(*)(const UserType&);
static constexpr auto GetFromJsonConverter() -> FromJsonFuncPtrType
{
if constexpr(HasFromJsonUserCast<UserType, ExternalJsonConverters>)
{ return &ExternalJsonConverters::fromJson; }
else
{ return &fromJsonInternal<UserType>; };
}
static constexpr auto GetToJsonConverter() -> ToJsonFuncPtrType
{
if constexpr(HasToJsonUserCast<UserType, ExternalJsonConverters>)
{ return &ExternalJsonConverters::toJson; }
else
{ return &toJsonInternal<UserType>; };
}
public:
inline static constexpr const FromJsonFuncPtrType fromJson = GetFromJsonConverter();
inline static constexpr const ToJsonFuncPtrType toJson = GetToJsonConverter();
};
So the caller will be free to serialize his types and internal types in consistent way
template <class UserType>
inline auto attempt()
{
rapidjson::Value json;
UserType val;
Converters::Distinct<UserType, UserJsonConversions>::fromJson(val, json);
Converters::Distinct<UserType, UserJsonConversions>::toJson(val);
}
inline void f()
{
attempt<sf::Uint32>();
attempt<sf::VideoMode>();
}