15

Consider the code:

template <typename T>
CByteArray serialize(const T& value)
{
    if (std::is_pod<T>::value)
        return serializePodType(value);
    else if (std::is_convertible<T, Variant>::value)
        return serialize(Variant(value));
    else
    {
        assert(0 == "Unsupported type");
        return CByteArray();
    }
}

Obviously, the compiler is right to give me this warning for if (std::is_pod<T>::value) etc., but how do I circumvent this? I can't find a way to avoid this check, and there's no static if in C++ (yet).

Can SFINAE principle be used to avoid this if?

Violet Giraffe
  • 29,070
  • 38
  • 172
  • 299

5 Answers5

10

Can SFINAE principle be used to avoid this if?

Yes, at least for the non-default cases:

template <typename T>
typename std::enable_if<std::is_pod<T>::value, CByteArray>::type 
serialize(const T& value)
{
    return serializePodType(value);
}

template <typename T>
typename std::enable_if<
    !std::is_pod<T>::value &&    // needed if POD types can be converted to Variant
    std::is_convertible<T, Variant>::value, CByteArray>::type 
serialize(const T& value)
{
    return serialize(Variant(value));
}

If you want a run-time, rather than compile-time, error for unsupported types, then declare a variadic function to catch any arguments that don't match the other overloads.

CByteArray serialize(...)
{
    hlassert_unconditional("Unsupported type");
    return CByteArray();
}
Mike Seymour
  • 235,407
  • 25
  • 414
  • 617
  • I actually want a compile-time error (who doesn't love early error detection?), and I would use `static_if` instead of `assert`, if C++ had such construct. – Violet Giraffe Oct 31 '14 at 14:18
  • @VioletGiraffe: In that case, just leave out the default case, and you'll get a compile-time error. There is `static_assert` as another way to conditionally cause a compile-time error; but you don't need that here. – Mike Seymour Oct 31 '14 at 14:20
  • @MikeSeymour The last overload doesn't need SFINAE... you could just do `CByteArray serialize(...) { assert(); return CByteArray(); }`. It'll only get picked if every other overload fails. – Barry Oct 31 '14 at 14:40
  • @Barry: Of course, I'd forgotten that detail. Thanks. – Mike Seymour Oct 31 '14 at 14:41
  • @Barry: It is UB to pass object to ellipses, no ? – Jarod42 Oct 31 '14 at 15:12
  • @Jarod42: No, not UB. "Passing a potentially-evaluated argument of class type [...] is conditionally-supported with implementation-defined semantics." It's not evaluated here. – Mike Seymour Oct 31 '14 at 15:22
  • Reading this code btw really makes me wish there was such a thing as `static if`. – Barry Oct 31 '14 at 15:35
  • @Barry: Yes, SFINAE does tend to melt my brain. In the future [Concepts](http://en.wikipedia.org/wiki/Concepts_(C%2B%2B)), a simpler syntax for constraints on template parameters, might make this sort of thing less painful. – Mike Seymour Oct 31 '14 at 15:39
6

You may use something like:

template <typename T> CByteArray serialize(const T& value);

namespace detail
{
    template <typename T>
    CByteArray serializePod(const T& value, std::true_type);
    {
        return serializePodType(value);
    }

    template <typename T>
    CByteArray serializePod(const T& value, std::false_type);
    {
        static_assert(std::is_convertible<T, Variant>::value, "unexpect type");
        return serialize(Variant(value));
    }
}

template <typename T>
CByteArray serialize(const T& value)
{
    return detail::serializePod(value, std::is_pod<T>{});
}
Jarod42
  • 173,454
  • 13
  • 146
  • 250
5

I'd be tempted to leave it as is, frankly. The compiler is demonstrating that it knows the unused branches can be optimised away. Sure, the warning is a bit of a drag, but ..

Anyway, if you really want to do this, use std::enable_if on the function's return type.

Lightness Races in Orbit
  • 358,771
  • 68
  • 593
  • 989
5

How's about this? http://ideone.com/WgKAju

#include <cassert>
#include <type_traits>
#include <iostream>

class CByteArray { public: CByteArray() {}};

class Variant {};

template<typename T>
CByteArray serializePodType(const T&)
{
    printf("serializePodType\n");
    return CByteArray();
}

CByteArray serializeVariant(const Variant& v)
{
    printf("serializeVariant\n");
    return CByteArray();
}

template <typename T>
typename std::enable_if<std::is_pod<T>::value, CByteArray>::type serialize(const T& value)
{
    return serializePodType(value);
}

template <typename T>
typename std::enable_if<std::is_convertible<T, Variant>::value && !std::is_pod<T>::value, CByteArray>::type serialize(const T& value)
{
    return serializeVariant(Variant(value));
}

class ConvertibleToVariant : public Variant { virtual void foo(); };

struct POD {};

struct NonPOD { virtual void foo(); };

int main()
{
    POD pod;
    ConvertibleToVariant ctv;
    //NonPOD nonpod;

    const auto ctv_serialised = serialize(ctv);
    const auto pod_serialised = serialize(pod);
    //const auto nonpod_serialised = serialize(nonpod);
}

This documentation is quite nice for enable_if: http://en.cppreference.com/w/cpp/types/enable_if

Ben Hymers
  • 22,821
  • 15
  • 55
  • 79
  • Damn, Mike beat me to it by like 8 minutes. That's what I get for posting answers after a lunchtime pub trip. – Ben Hymers Oct 31 '14 at 14:27
2

Now, you can use template constraints to fix it, I like to use a little macro to help make the enable_if boilerplate a little clearer:

#define REQUIRES(...) typename std::enable_if<(__VA_ARGS__), int>::type = 0

Then you could define them directly in the function:

template <typename T, REQUIRES(std::is_pod<T>::value)>
CByteArray serialize(const T& value)
{
    return serializePodType(value);
}

template <typename T, REQUIRES(
    !std::is_pod<T>::value &&
    !std::is_convertible<T, Variant>::value
)>
CByteArray serialize(const T& value)
{
    assert(0 == "Unsupported type");
    return CByteArray();
}

// This is put last so `serialize` will call the other overloads
template <typename T, REQUIRES(
    !std::is_pod<T>::value &&
    std::is_convertible<T, Variant>::value
)>
CByteArray serialize(const T& value)
{
    return serialize(Variant(value));
}

However, this gets ugly very quickly. First, you have to negate the other conditions to avoid ambiguity. Secondly, the functions have to be ordered so that the other functions are declared or defined before they are called recursively. It doesn't really scale well. If you need to add additional conditions in the future, it can get a lot more complicated.

A better solution is to use conditional overloading with a fix point combinator. The Fit library provides a conditional and fix adaptor, so you don't have to write your own. So in C++14, you could write:

const constexpr serialize = fit::fix(fit::conditional(
    FIT_STATIC_LAMBDA(auto, const auto& value, 
        REQUIRES(std::is_pod<decltype(value)>()))
    {
        return serializePodType(value);
    },
    FIT_STATIC_LAMBDA(auto self, const auto& value, 
        REQUIRES(std::is_convertible<decltype(value), Variant>()))
    {
        return self(Variant(value));
    },
    FIT_STATIC_LAMBDA(auto, const auto&)
    {
        assert(0 == "Unsupported type");
        return CByteArray();
    }
));

However, if you aren't using C++14 yet, you will have to write them as function objects instead:

struct serialize_pod
{
    template<class Self, class T, 
        REQUIRES(std::is_pod<T>::value)>
    CByteArray operator()(Self, const T& value) const
    {
        return serializePodType(value);
    }
};

struct serialize_variant
{
    template<class Self, class T, 
        REQUIRES(std::is_convertible<T, Variant>::value)>
    CByteArray operator()(Self self, const T& value) const
    {
        return self(Variant(value));
    }
};

struct serialize_else
{
    template<class Self, class T>
    CByteArray operator()(Self, const T&) const
    {
        assert(0 == "Unsupported type");
        return CByteArray();
    }
};

const constexpr fit::conditional_adaptor<serialize_pod, serialize_variant, serialize_else> serialize = {};

Finally, for your case specifically, you could drop the else part unless you really have a need for a runtime check. Then you can just have the two overloads:

const constexpr serialize = fit::fix(fit::conditional(
    FIT_STATIC_LAMBDA(auto, const auto& value, 
        REQUIRES(std::is_pod<decltype(value)>()))
    {
        return serializePodType(value);
    },
    FIT_STATIC_LAMBDA(auto self, const auto& value, 
        REQUIRES(std::is_convertible<decltype(value), Variant>()))
    {
        return self(Variant(value));
    }
));

So you will have a compiler error instead. The nice thing about using enable_if and constraints is that the error will be in the user code instead of in your code(with some long backtrace). This helps make it clear that the user is the one making the error instead of a problem with the library code.

Paul Fultz II
  • 16,112
  • 11
  • 51
  • 58