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?

6
  • Maybe there is a #pragma to disable such warnings? like here? Commented Oct 31, 2014 at 14:02
  • @Borgleader: sure, but it's not an option for me since this code will be compiled for Windows with MSVC, for Linux and Android with GCC, for Mac and iOS with clang... Commented Oct 31, 2014 at 14:04
  • 1
    You may use tag dispatching also. Commented Oct 31, 2014 at 14:10
  • @Jarod42: Actually I think you may have to; with std::enable_if I've written myself into a corner with ambiguous overloads. Commented Oct 31, 2014 at 14:15
  • What optimization level are you compiling? Commented Oct 31, 2014 at 20:19

5 Answers 5

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();
}
Sign up to request clarification or add additional context in comments.

8 Comments

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.
@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.
@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: Of course, I'd forgotten that detail. Thanks.
@Barry: It is UB to pass object to ellipses, no ?
|
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>{});
}

Comments

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.

1 Comment

Sorry, I've read the enable_if description but I don't understand how you're proposing to use it in this case.
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

1 Comment

Damn, Mike beat me to it by like 8 minutes. That's what I get for posting answers after a lunchtime pub trip.
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.

1 Comment

Cool trick, the first one. The rest is too complicated, in my opinion.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.