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:
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.
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.
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
.
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;
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With