Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Partial specialization of class template for a type that appears in any position of a variadic template parameter pack

I have defined a type that acts as an integer. I want to define a specialization for std::common_type for my type. However, this specialization should be able to give the common_type of bounded_integer (my class) in combination with any number of other arguments that are either other bounded_integer or built-in integer types. I want the following code to all be valid:

std::common_type<bounded_integer<1, 10>>::type
std::common_type<bounded_integer<1, 10>, int>::type
std::common_type<int, long, bounded_integer<1, 10>>::type
std::common_type<int, int, long, short, long long, short, bounded_integer<1, 10>, int, short, short, short, ..., short, bounded_integer<1, 10>>::type

My first attempt at solving this problem was by using enable_if. However, I realized that this would not allow me to distinguish from the library definition of common_type, as what I had was essentially

#include <type_traits>

class C {};

template<typename T, typename... Ts>
class contains_c {
public:
        static constexpr bool value = contains_c<T>::value or contains_c<Ts...>::value;
};
template<typename T>
class contains_c<T> {
public:
        static constexpr bool value = std::is_same<T, C>::value;
};

namespace std {

template<typename... Args, typename std::enable_if<contains_c<Args...>::value>::type>
class common_type<Args...> {
public:
        using type = C;
};

}       // namespace std

int main() {
}

Where the 'partial specialization' is really just "any arguments", which is no more specialized than what we have.

So it seems like the only solution is to require my users to do one of the following:

  1. always put the bounded_integer as the first argument to common_type
  2. always use my make_bounded(built-in integer value) function to convert their integers to bounded_integer (so don't have a specialization of common_type for built-in types in combination with bounded_integer)
  3. never put bounded_integer in a position greater than N, where N is some number I determine, similar to Visual Studio's old variadic template work-around

3 would look something like this:

// all_bounded_integer_or_integral and all_are_integral defined elsewhere with obvious definitions
template<intmax_t minimum, intmax_t maximum, typename... Ts, typename = type std::enable_if<all_bounded_integer_or_integral<Ts...>::value>::type>
class common_type<bounded_integer<minimum, maximum>, Ts...> {
};
template<typename T1, intmax_t minimum, intmax_t maximum, typename... Ts, typename = typename std::enable_if<all_are_integral<T1>::value>::type, typename = typename std::enable_if<all_bounded_integer_or_builtin<Ts...>::value>::type>
class common_type<T1, bounded_integer<minimum, maximum>, Ts...> {
};
template<typename T1, typename T2, intmax_t minimum, intmax_t maximum, typename... Ts, typename = typename std::enable_if<all_are_integral<T1, T2>::value>::type, typename = typename std::enable_if<all_bounded_integer_or_builtin<Ts...>::value>::type>
class common_type<T1, T2, bounded_integer<minimum, maximum>, Ts...> {
};
// etc.

Is there a better way to accomplish this (template specialization when all the types meet one condition and any of the types meet another condition) for a class that I cannot change the original definition for?

EDIT:

Based on the answers, I was not clear enough in my problem.

First, expected behavior:

If someone calls std::common_type with all of the types being an instance of bounded_integer or a built-in numeric type, I want the result to be a bounded_integer that has a minimum of all of the possible minimums and a maximum of all of the possible maximums.

The problem:

I have a working solution when someone calls std::common_type on any number of bounded_integer. However, if I only specialize the two-argument version, then I run into the following problem:

std::common_type<int, unsigned, bounded_integer<0, std::numeric_limits<unsigned>::max() + 1>

should give me

bounded_integer<std::numeric_limits<int>::min(), std::numeric_limits<unsigned>::max() + 1>

However, it does not. It first applies common_type to int and unsigned, which follows the standard integral promotion rules, giving unsigned. Then it returns the result of common_type with unsigned and my bounded_integer, giving

bounded_integer<0, std::numeric_limits<unsigned>::max() + 1>

So by adding unsigned to the middle of the parameter pack, even though it should have absolutely no impact on the result type (its ranges are entirely contained within the ranges of all other types), it still affects the result. The only way I can think of to prevent this is to specialize std::common_type for any number of built-in integers followed by bounded_integer, followed by any number of built-in integers or bounded_integer.

My question is: how can I do this without having to approximate it by manually writing out an arbitrary number of parameters followed by a bounded_integer followed by a parameter pack, or is this not possible?

EDIT 2:

The reason that common_type will give the wrong values can be explained by this reasoning following the standard (quoting from N3337)

The common_type of int and unsigned is unsigned. For an example: http://ideone.com/9IxKIW . Standardese can be found in § 20.9.7.6/3, where the common_type of two values is

typedef decltype(true ? declval<T>() : declval<U>()) type;

In § 5.16/6, it says

The second and third operands have arithmetic or enumeration type; the usual arithmetic conversions are performed to bring them to a common type, and the result is of that type.

The usual arithmetic conversions are defined in § 5/9 as

Otherwise, if the operand that has unsigned integer type has rank greater than or equal to the rank of the type of the other operand, the operand with signed integer type shall be converted to the type of the operand with unsigned integer type.

like image 957
David Stone Avatar asked Sep 08 '13 00:09

David Stone


3 Answers

std::common_type extrapolates its own two-argument specialization into the n-argument case. You only need to specialize the two-argument cases.

template< typename other, int low, int high >
struct common_type< other, ::my::ranged_integer< low, high > > {
    using type = other;
};

template< typename other, int low, int high >
struct common_type< ::my::ranged_integer< low, high >, other > {
    using type = other;
};

template< int low, int high >
struct common_type< ::my::ranged_integer< low, high >,
                    ::my::ranged_integer< low, high > > {
    using type = ::my::ranged_integer< low, high >;
};

This leaves undefined the common_type between different ranged integers. I suppose you could do it with min and max.

You could also make an is_ranged_integer trait if your class supports inheritance.

Don't forget to put your library inside a namespace.

like image 114
Potatoswatter Avatar answered Sep 26 '22 09:09

Potatoswatter


Short answer

If using std::common_type as provided by the standard library is absolutely required, there's no better way other than the 3 alternatives you observed yourself. If a user defined common_type be considered acceptable, then you can achieve what you want as shown below.


Long Answer

You are right when you say std::common_type<unsigned [long [long]] int, [long [long]] int>::type will yield unsigned [long [long]]. However, since the common_ type of any expression involving ranged_integer is itself a ranged_integer and given that your specializations involving ranged_integer correctly infer the ranges, there's only a problem if the pairwise common_type of the types preceding [long [long]] unsigned yields [long [long]] int.T̶h̶a̶t̶ ̶l̶e̶a̶v̶e̶s̶ ̶u̶s̶ ̶o̶n̶l̶y̶ ̶s̶i̶x̶ ̶c̶a̶s̶e̶s̶ ̶w̶e̶ ̶h̶a̶v̶e̶ ̶t̶o̶ ̶w̶o̶r̶k̶a̶r̶o̶u̶n̶d̶,̶ ̶n̶a̶m̶e̶l̶y̶ ̶̶s̶t̶d̶:̶:̶c̶o̶m̶m̶o̶n̶_̶t̶y̶p̶e̶<̶u̶n̶s̶i̶g̶n̶e̶d̶ ̶[̶l̶o̶n̶g̶ ̶[̶l̶o̶n̶g̶]̶]̶ ̶i̶n̶t̶,̶ ̶[̶l̶o̶n̶g̶ ̶[̶l̶o̶n̶g̶]̶]̶ ̶i̶n̶t̶>̶:̶:̶t̶y̶p̶e̶̶ ̶a̶n̶d̶ ̶t̶h̶e̶i̶r̶ ̶o̶r̶d̶e̶r̶i̶n̶g̶ ̶p̶e̶r̶m̶u̶t̶a̶t̶i̶o̶n̶s̶.̶ ̶(̶I̶'̶m̶ ̶i̶g̶n̶o̶r̶i̶n̶g̶ ̶t̶h̶e̶ ̶f̶i̶x̶e̶d̶ ̶w̶i̶d̶t̶h̶ ̶t̶y̶p̶e̶s̶ ̶h̶e̶r̶e̶,̶ ̶b̶u̶t̶ ̶t̶h̶e̶ ̶e̶x̶t̶e̶n̶d̶i̶n̶g̶ ̶t̶h̶e̶ ̶i̶d̶e̶a̶ ̶t̶o̶ ̶c̶o̶n̶s̶i̶d̶e̶r̶ ̶t̶h̶e̶m̶ ̶s̶h̶o̶u̶l̶d̶ ̶b̶e̶ ̶s̶t̶r̶a̶i̶g̶h̶t̶f̶o̶r̶w̶a̶r̶d̶)̶

W̶e̶ ̶c̶a̶n̶ ̶a̶c̶h̶i̶e̶v̶e̶ ̶t̶h̶a̶t̶ ̶b̶y̶ ̶a̶g̶a̶i̶n̶ ̶p̶r̶o̶v̶i̶d̶i̶n̶g̶ ̶e̶x̶p̶l̶i̶c̶i̶t̶ ̶s̶p̶e̶c̶i̶a̶l̶i̶z̶a̶t̶i̶o̶n̶s̶:̶

In fact we can't according to n3485

[meta.type.synop] Paragraph 1

"The behavior of a program that adds specializations for any of the class templates defined in this subclause [template <class... T> common_type included] is undefined unless otherwise specified."

[meta.trans.other] Table 57

[...] A program may specialize this trait [template <class... T> common_type] if at least one template parameter in the specialization is a user-defined type. [...]"

That implies that there's no valid way of overwriting the behavior for std::common_type<unsigned [long [long]] int, [long [long]] int>::type, it is required by the standard to always yield unsigned [long [long]] int as pointed out before.


Alternative to std::common_type

An alternative to overcome the limitations of std::common_type when applied to primitive integral types, is to define a custom common_type.

Assuming ranged_integer to be defined as follows.

template<typename T, T min, T max>
struct basic_ranged_integer;

template<std::intmax_t min, std::intmax_t max>
using ranged_integer = basic_ranged_integer<std::intmax_t, min, max>;

A custom common_type could be defined as follows.

First the left recursion:

template<typename... T>
struct common_type;

template<typename T, typename U, typename... V>
struct common_type<T, U, V...> :
        common_type<typename common_type<T, U>::type, V...> //left recursion
{};

Now the specializations involving basic_ranged_integer.

//two basic_ranged_integer
template<typename T, T minT, T maxT, typename U, U minU, U maxU>
struct common_type<basic_ranged_integer<T, minT, maxT>, basic_ranged_integer<U, minU, maxU>>
{
    //gory details go here
};

//basic_ranged_integer mixed with primitive integer types
//forwards to the case involving two basic_ranged_integer
template<typename T, T minT, T maxT, typename U>
struct common_type<basic_ranged_integer<T, minT, maxT>, U> :
        common_type
        <
            basic_ranged_integer<T, minT, maxT>,
            typename make_ranged_integer<U>::type
        >
{};

template<typename T, typename U, U minU, U maxU>
struct common_type<T, basic_ranged_integer<U, minU, maxU>> :
        common_type
        <
            typename make_ranged_integer<T>::type,
            basic_ranged_integer<U, minU, maxU>
        >
{};

And finally the specializations involving a combination of signed and unsigned primitive integers.

//base case: forwards to the satandard library
template<typename T>
struct common_type<T> :
        std::common_type<T>
{};

template<typename T, typename U>
struct common_type<T, U>
{
    static constexpr bool signed_xor  = std::is_signed<T>{} xor std::is_signed<U>{};

    //base case: forwards to the satandard library
    template<bool b = signed_xor, typename = void>
    struct helper :
            std::common_type<T, U>
    {};

    //mixed signed/unsigned: forwards to the case involving two basic_ranged_integer
    template<typename _ >
    struct helper<true, _> :
            common_type<typename make_ranged_integer<T>::type, typename make_ranged_integer<U>::type>
    {};

    using type = typename helper<>::type;
};

In the above make_ranged_integer is expected to take a primitive integer type and define type to be the desired corresponding basic_ranged_integer.

like image 22
brunocodutra Avatar answered Sep 22 '22 09:09

brunocodutra


Here's a possible implementation:

#include <limits>  
#include <utility>
#include <iostream>

template<typename T, typename U>
static constexpr auto min(T x, U y) -> decltype(x < y ? x : y)
{
    return x < y ? x : y;
}

template<typename T, typename U>
static constexpr auto max(T x, U y) -> decltype(x < y ? x : y)
{
    return x > y ? x : y;
}

template<intmax_t f, intmax_t l>
struct ranged_integer
{
    static intmax_t const first = f;
    static intmax_t const last  = l;
    static_assert(l > f, "invalid range");
};

template <class ...T> struct common_type
{
};

template <class T>
struct common_type<T>
{
    typedef T type;
};

template <class T, class U>
struct common_type<T, U>
{
    typedef decltype(true ? std::declval<T>() : std::declval<U>()) type;
};

template <class T, intmax_t f, intmax_t l>
struct common_type<T, ranged_integer<f,l>>
{
    typedef ranged_integer< min(std::numeric_limits<T>::min(),f) , max(std::numeric_limits<T>::max(),l) > type;
};

template <class T, intmax_t f, intmax_t l>
struct common_type<ranged_integer<f,l>, T>
{
    typedef typename common_type<T, ranged_integer<f,l>>::type type;
};

template <intmax_t f1, intmax_t l1, intmax_t f2, intmax_t l2>
struct common_type<ranged_integer<f1,l1>, ranged_integer<f2,l2>>
{
    typedef ranged_integer< min(f1,f2) , max(l1,l2) > type;
};

template <class T, class U, class... V>
struct common_type<T, U, V...>
{
    typedef typename common_type<typename common_type<T, U>::type, V...>::type type;
};

int main(int argc, char *argv[])
{
    typedef common_type<char, ranged_integer<-99999999, 20>, short, ranged_integer<10, 999999999>, char>::type type;
    std::cout << type::first << std::endl; // -99999999
    std::cout << type::last << std::endl;  // 999999999
    return 0;
}
like image 22
a.lasram Avatar answered Sep 26 '22 09:09

a.lasram