Search code examples
c++templatessfinaeif-constexpr

SFINAE User-Defined-Conversion Operator


I'm trying to do a templated user-defined-conversion that uses design by introspection.

In C++20 I can do the following:

    template<typename T>
    operator T() const
    {
        if constexpr( requires { PFMeta<T>::from_cursor(*this); } )
        {
            return PFMeta<T>::from_cursor(*this);
        }
        else
        {
            T x;
            std::memcpy(&x, &data, sizeof(x));
            return x;
        }
    }

i.e. if PFMeta<T>::from_cursor(const struct PFCursor&) is defined then use it else do a memcpy. (longer example here in godbolt: https://godbolt.org/z/abed1vsK3)

I love this approach but unfortunately this library will need to work on C++17 too.

So I've been trying SFINAE as an alternative to the concepts but it's very tricky. I finally managed to get something similar but with a templated method as rather than the user-defined-conversion operator itself:

template<typename T, typename = void>
struct has_from_cursor : std::false_type { };

template<typename T>
struct has_from_cursor<T, decltype( PFMeta<T>::from_cursor(std::declval<struct PFCursor>()), void() ) > : std::true_type { };

// ...

    template<class T>
    std::enable_if_t<has_from_cursor<T>::value, T> as() const
    {
        return PFMeta<T>::from_cursor(*this);
    }

    template<class T>
    std::enable_if_t<!has_from_cursor<T>::value, T> as() const
    {
        T x;
        std::memcpy(&x, &data, sizeof(x));
        return x;
    }

I tried the following which compiles but does not work (I can't cast with it):

    template<typename T>
    operator std::enable_if_t<has_from_cursor<T>::value, T>() const
    {
        return PFMeta<T>::from_cursor(*this);
    }

    template<typename T>
    operator std::enable_if_t<!has_from_cursor<T>::value, T>() const
    {
        T x;
        std::memcpy(&x, &data, sizeof(x));
        return x;
    }

Longer example here: https://godbolt.org/z/5r9Mbo18h

So two questions:

  • Can I do effectively what I can do with concepts with just SFINAE in C++17 for the user defined conversion operator?
  • Is there a simpler way to do the as approach than the type traits & enable_if? Ideally I'd like to do something like define the default templated method and then have a specialisation to be preferred if the condition is there (i.e. there is a meta class with a static member function defined).

Thanks!


Solution

  • Can I do effectively what I can do with concepts with just SFINAE in C++17 for the user defined conversion operator?

    Consider that C++17 support if constepr. Given that you've developed a has_from_cursor custom type traits that inherit from std::true_type or from std::false_type, you can use it for if constexp.

    I mean (caution: code not tested)

    template<typename T>
    operator T() const
    { // .............VVVVVVVVVVVVVVVVVVVVVVVVV
        if constexpr( has_from_cursos<T>::value )
        {
            return PFMeta<T>::from_cursor(*this);
        }
        else
        {
            T x;
            std::memcpy(&x, &data, sizeof(x));
            return x;
        }
    }
    

    Is there a simpler way to do the as approach than the type traits & enable_if? Ideally I'd like to do something like define the default templated method and then have a specialisation to be preferred if the condition is there (i.e. there is a meta class with a static member function defined).

    I suppose you can try tag-dispatching...

    I mean something as calling 'as()', from operator T() with an additional int argument

    template<typename T>
    operator T() const
    { return as<T>(0); } // <-- call as() with a int
    

    where there is a as() specific for from_cursos class enabled, that receive an unused int and is simply SFINAE enabled/disabled through decltype()

    template <typename T>  // ........accept an int; best match
    decltype(PFMeta<T>::from_cursor(*this)) as (int) const
     { return PFMeta<T>::from_cursor(*this); }
    

    and a generic as(), receiving a long

    template <typename T>
    T as (long) const // <-- accept a long; worst match
     {
       T x;
       std::memcpy(&x, &data, sizeof(x));
       return x;
     }
    

    The trick is the unused argument: a int.

    When the specialized as() is enabled, if preferred because accept a int so is a better match.

    When the specialized as() is disabled, remain the generic as() as better-than-nothig match.