Partial specialization of a class template for a type that appears at any position in a package of parameters of a variational template

I 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 provide the common_type bounded_integer (my class) in combination with any number of other arguments, which are either other bounded integer or inline integer types. I want the following code to 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 to solve this problem was with enable_if. However, I realized that this would not allow me to distinguish from the definition of the common_type library, since 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() { } 

If “partial specialization” is simply “any arguments” that are not more specialized than what we have.

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

  • always puts bounded_integer as the first argument to common_type
  • always use the make_bounded function (built-in integer value) to convert your integers to bounded_integer (so you don't have the common_type specialization for built-in types combined with bounded_integer)
  • never put bounded_integer in a position greater than N, where N is some number that I define, similar to the old work with Visual Studio template templates

3 will 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 achieve this (template specialization, when all types satisfy one condition, and any of the types meets another condition) for a class that I cannot change the original definition for?

EDIT:

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

First, the expected behavior:

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

Problem:

I have a working solution when someone calls std :: common_type on any number of bounded_integer. However, if I am only specializing in a version with two arguments, then I am faced with 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 is not. First, he applies common_type to int and unsigned , which follows the standard integral promotion rules, providing unsigned . It then returns the result of common_type with unsigned and my bounded_integer , giving

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

So, adding unsigned to the middle of the package of parameters, although it should have absolutely no effect on the type of result (its ranges are completely contained in the ranges of all other types), it still affects the result. The only way 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 the need for approximation, manually writing out an arbitrary number of parameters, followed by a bounded_integer , followed by a package of parameters, or is this impossible?

EDIT 2:

The reason common_type will give incorrect values ​​can be explained by these standard considerations (quoted from N3337)

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

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

§ 5.16 / 6 states that

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

Ordinary arithmetic conversions are defined in § 5/9 as

Otherwise, if the operand having an unsigned integer type has a rank greater than or equal to the ranks of the type of the other operand, the operand with a signed integer type must be converted to the operand type with an unsigned integer.

+7
c ++ c ++ 11 template-meta-programming template-specialization variadic-templates
source share
4 answers

std::common_type extrapolates its own specialization with two arguments to the case of an n-argument. You only need to highlight two arguments.

 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 common_type between different integers. I suppose you could do this with min and max .

You can also create the is_ranged_integer flag if your class supports inheritance.

Remember to place your library in the namespace.

+3
source share

Short answer

If using std::common_type , as provided by the standard library, is absolutely necessary, there is no better way than the three alternatives that you yourself have observed. If the user defined by common_type is considered acceptable, 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 , you get unsigned [long [long]] . However, since the common_ type any expression that includes ranged_integer is ranged_integer , and given that your specializations containing ranged_integer correctly define ranges, there is only a problem if the pair common_type types preceding [long [long]] unsigned gives [long [long]] int .That leaves us only six cases, we have to solve this problem, ̶ ̶ Namely the ̶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̶ and their systematization ̶p̶e̶r̶m̶u̶t̶a̶t̶i̶o̶n̶s̶.̶ ̶ (̶I̶'̶m̶ Ignoring Fixed Width TYPES Here ̶ but expanding the idea to consider ̶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̶a

In fact, we cannot by n3485

[meta.type.synop] Item 1

"The behavior of a program that adds specializations to 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 in this feature [ template <class... T> common_type ] if at least one template parameter in the specialization is a user-defined type. [...] "

This means that there is no acceptable way to overwrite behavior for std::common_type<unsigned [long [long]] int, [long [long]] int>::type , the standard should always give unsigned [long [long]] int , as indicated earlier.


Alternative std::common_type

An alternative to overcoming the limitations of std::common_type for primitive integral types is to define a custom common_type .

Assuming ranged_integer is 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>; 

Custom common_type can be defined as follows.

First 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 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, specializations associated with 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 it is assumed to take a primitive integer type and define type as the desired corresponding basic_ranged_integer .

+2
source share

Here, execution is possible:

 #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; } 
+1
source share

Maybe I missed something, but you just don't need something similar for the int case:

 namespace std { // first give ranged_integer a ground zero template<intmax_t minimum, intmax_t maximum> class common_type<ranged_integer<minimum, maximum>> { typedef typename ranged_integer<minimum, maximum> type; }; // sort out int template<intmax_t minimum, intmax_t maximum> class common_type<int, ranged_integer<minimum, maximum>> { typedef typename ranged_integer<minimum, maximum> type; }; template<intmax_t minimum, intmax_t maximum> class common_type<ranged_integer<minimum, maximum>, int>> { typedef typename ranged_integer<minimum, maximum> type; }; 

repeat this for `long , long long , etc ... and common_type definition of the template take care of all variation cases with only one type of ranged_integer ?

0
source share

All Articles