Skip to content

Type Constraints Unraveled

Eric Voskuil edited this page Jun 8, 2021 · 47 revisions

If one is not familiar with C++ template overload resolution, the use of enable_if and enable_if_t to constrain template parameters can be a little hard to follow. While these are provided by C++11 and C++14 respectively, they are trivially implemented as follows.

// C++11 implementation of std::enable_if.
template<bool Bool, typename Type = void>
struct enable_if
{
};
 
template<class Type>
struct enable_if<true, Type>
{
    typedef Type type;
};

// C++14 implementation of use std::enable_if_t.
template <bool Bool, typename Type = void>
using enable_if_t = typename enable_if<Bool, Type>::type;

The following helpers combine signed-ness with integer-ness (i.e. floating point excluded).

#include <type_traits>

template <typename Type>
using if_integer = enable_if_t<
    std::numeric_limits<Type>::is_integer, bool>;

template <typename Type>
using if_signed_integer = enable_if_t<
    std::numeric_limits<Type>::is_integer &&
    std::is_signed<Type>::value, bool>;

template <typename Type>
using if_unsigned_integer = enable_if_t<
    std::numeric_limits<Type>::is_integer &&
    !std::is_signed<Type>::value, bool>;

The std::numeric_limits<Type>::is_integer template requires C++11 for long long, unsigned long long and some character types. For older versions of C++ this may be implemented with simple custom templates that enumerate these types.

When the Bool parameter of enable_if_t<bool Bool, typename Type> is true, enable_if_t resolves to the specified Type, in the above cases bool. Otherwise it resolves to the undefined expression (struct enable_if{})::type. The former is then defaulted (i.e. using = true or = false) so that it is not required. The latter will not match any expression, so that case is excluded.

Using enable_if_t

The following is_negative overloads provide an example.

template <typename Integer, if_signed_integer<Integer> = true>
bool is_negative(Integer value)
{
    return value < 0;
}

template <typename Integer, if_unsigned_integer<Integer> = true>
bool is_negative(Integer value)
{
    return false;
}

These reduce to the following.

// if (std::numeric_limits<Type>::is_integer && std::is_signed<Integer>::value)
template <typename Integer, bool = true>
bool is_negative(Integer value)
{
    return value < 0;
}

// if (std::numeric_limits<Type>::is_integer && !std::is_signed<Integer>::value)
template <typename Integer, bool = true>
bool is_negative(Integer value)
{
    return false;
}

A side effect of this technique is that the signature of is_negative is actually is_negative<Integer, bool>(Integer value), where a bool value is ignored if specified.

Using enable_if

Another approach is to reply on enable_if alone.

template <typename Integer,
    typename Unused = enable_if<std::numeric_limits<Type>::is_integer>::type>
bool is_odd(Integer value)
{
    return (value % 2) != 0;
}

As the Unused type may be unnamed, this may also be written as follows.

template <typename Integer,
    typename = enable_if<std::numeric_limits<Type>::is_integer>::type>
bool is_odd(Integer value)
{
    return (value % 2) != 0;
}

This resolves to the following.

// if (std::numeric_limits<Type>::is_integer)
template <typename Integer,
    typename = (struct enable_if{ typedef bool type; })::type>
bool is_odd(Integer value)
{
    return (value % 2) != 0;
}

// if (!std::numeric_limits<Type>::is_integer)
template <typename Integer,
    typename = (struct enable_if{})::type>
bool is_odd(Integer value)
{
    return (value % 2) != 0;
}

And these reduce to the following.

// if (std::numeric_limits<Type>::is_integer)
template <typename Integer, typename = bool>
bool is_odd(Integer value)
{
    return (value % 2) != 0;
}

// if (!std::numeric_limits<Type>::is_integer)
template <typename Integer, typename = undefined>
bool is_odd(Integer value)
{
    return (value % 2) != 0;
}

The bool type is inferred from the expression std::numeric_limits<Type>::is_integer which was passed to enable_if, exposed by enable_if via its ::type declaration, and then dereferenced by ::type in the template declaration.

As the latter will not match any expression, the former remains. Therefore the signature is actually is_odd<Integer, typename = bool>(Integer value), where the second template parameter may be any type and is ignored if specified.

Common Mistakes

These compile but do not constrain the type.

template <typename Integer, typename = enable_if_t<std::numeric_limits<Integer>::is_integer>>
bool is_odd(Integer value)
{
    return (value % 2) != 0;
}

template <typename Integer, enable_if<std::numeric_limits<Integer>::is_integer>::type = true>
bool is_odd(Integer value)
{
    return (value % 2) != 0;
}

These reduce respectively to the following, under all conditions.

template <typename Integer, typename = bool>
bool is_odd(Integer value)
{
    return (value % 2) != 0;
}

template <typename Integer, bool = true>
bool is_odd(Integer value)
{
    return (value % 2) != 0;
}

Consequently, any type of value passed to is_odd(Type value) will compile if there is a Type % 2 operator overload for the type. Natively this includes all arithmetic types (including char and floating point) but may include others.

Libbitcoin Menu

Clone this wiki locally