Search code examples
c++c++20sfinaec++-conceptsdetection-idiom

How to correctly write and apply c++20 concept / SFINAE / both for function selecting via struct definition?


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?


Solution

  • 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>();
    }