Написание насыщенного оператора приведения без перечисления всех возможных комбинаций - PullRequest
10 голосов
/ 05 июня 2019

Я хотел бы создать шаблонные операции между различными типами (предположим, что это список: int8_t, int16_t, int32_t, int64_t, uint8_t, uint16_t, uint32_t, uint64_t, float, double).

Тогда я бы хотел, чтобы функция saturate_cast<>() принимала входное значение, проверяла, находится ли оно в пределах выходного типа, и насыщала до тех пределов, если это необходимо.

Проблема в том, что, если я суммирую два int32_t, операция C ++ по умолчанию имеет неопределенное поведение в случае переполнения, поэтому я бы хотел довести операцию до int64_t и использовать этот тип для выполнения операции.

Предварительное решение может быть:

#include <cstdint>
#include <limits>

template<typename T1, typename T2> struct type_which_fits { using type = decltype(T1() + T2()); };
template<> struct type_which_fits<int32_t, int32_t> { using type = int64_t; };

template<typename T1, typename T2>
auto TAdd(T1 lhs, T2 rhs) {
    using type = typename type_which_fits<T1, T2>::type;
    return static_cast<type>(lhs) + static_cast<type>(rhs);
}

template<typename ODT, typename IDT>
ODT saturate_cast(const IDT& v) {
    if (v > std::numeric_limits<ODT>::max()) {
        return std::numeric_limits<ODT>::max();
    }
    if (v < std::numeric_limits<ODT>::min()) {
        return std::numeric_limits<ODT>::min();
    }

    return static_cast<ODT>(v);
}

int main()
{
    auto x = saturate_cast<int8_t>(TAdd(1, 1u));
    return 0;
}

К сожалению, таким образом мне нужно дополнительно указать все возможные комбинации типов, и мне просто понадобятся эти правила (для проверки в заданном порядке):

  • если один из типов является плавающей точкой, используйте промоакции по умолчанию
  • если один из типов является 64-битным, повысить до удвоенного значения
  • если один из типов 32-битный, повысить до 64-битных
  • в противном случае используйте акции по умолчанию

Более того, в saturate_cast<>() всплывает куча предупреждений со знаком / без знака, когда два типа имеют разную "подпись".

Опять же, это можно решить, специализируясь на всех возможных комбинациях, но почему-то это кажется "неправильным".

Можете ли вы предложить решение, чтобы сделать это более гибким, в тот момент, когда мне понадобится больше типов?

1 Ответ

7 голосов
/ 05 июня 2019

Вот мой подход, выполнив шаги:

  1. Найдите улучшенный тип (тип, который может быть получен при выполнении операции)

  2. повысить тип до следующего типа в соответствии с вашими правилами 1-4

  3. Выполните сложение, одновременно приводя обе стороны к типу повышения.


Улучшенный тип можно найти по следующим общим правилам:

  • с плавающей точкой + все что угодно -> с плавающей точкой

  • если левый и правый биты имеют одинаковый размер, выберите больший (биты без знака со знаком)

  • еще выберите где sizeof() дает наибольшее

последние 2 шага можно гарантировать (и упростить), создав вспомогательную структуру, которая возвращает больший тип (ab), используя std::numerical_limits<T>::digits, который аккуратно делает именно то, что мы хотим (также относительно подписи / неподписания), так как:

  • std::numerical_limits<int>::digits -> 15

  • std::numerical_limits<unsigned>::digits -> 16

Это будет работать для всех арифметических типов соответственно.

template<typename T, typename U>
struct larger_arithmetic_type {
    static_assert(std::is_arithmetic_v<T>, "T must be arithmetic");
    static_assert(std::is_arithmetic_v<U>, "U must be arithmetic");
    using type = typename std::conditional_t<(std::numeric_limits<T>::digits < std::numeric_limits<U>::digits), U, T>;
};

template<typename T, typename U>
using larger_arithmetic_type_t = typename larger_arithmetic_type<T, U>::type;

С этим мы можем разрешить struct arithmetic_superior_type (следуя упомянутым общим правилам) находить улучшенный тип из целых чисел и / или чисел с плавающей точкой:

template<typename T, typename U>
struct arithmetic_superior_type {
    using type = typename 
        std::conditional_t<std::is_floating_point_v<T> && std::is_floating_point_v<U>, larger_arithmetic_type_t<T, U>,
        std::conditional_t<std::is_floating_point_v<T>, T, 
        std::conditional_t<std::is_floating_point_v<U>, U, 
        larger_arithmetic_type_t<T, U>>>>;

};

template<typename T, typename U>
using arithmetic_superior_type_t = typename arithmetic_superior_type<T, U>::type;

Следовательно, arithmetic_seperior_type_t<T, U> возвращает тип, который +, -, * и / между T и U даст:

arithmetic_superior_type_t<std::int32_t, float> a;          //-> float
arithmetic_superior_type_t<std::uint32_t, std::int32_t> b;  //-> std::uint32_t
arithmetic_superior_type_t<std::uint32_t, std::uint32_t> c; //-> std::uint32_t
arithmetic_superior_type_t<std::uint64_t, std::uint32_t> d; //-> std::uint64_t
arithmetic_superior_type_t<float, double> e;                //-> double
arithmetic_superior_type_t<std::uint16_t, std::int64_t> f;  //-> std::int64_t

Теперь, как вы сказали, одного этого типа недостаточно. Переполнение возможно, поэтому promote_superior_type - это шаг 2, чтобы получить повышение класса из T и U - типа, который определенно будет содержать любой результат добавления:

template<typename T, typename U>
struct promote_superior_type {
    using superior_type = arithmetic_superior_type_t<T, U>;

    using type = typename
        std::conditional_t<(sizeof(T) == 8u || sizeof(U) == 8u), double, 
        std::conditional_t<std::is_floating_point_v<superior_type>, superior_type, 
        std::conditional_t<(std::numeric_limits<superior_type>::digits < std::numeric_limits<std::int16_t>::digits), std::conditional_t<std::is_signed_v<superior_type>, std::int16_t, std::uint16_t>,
        std::conditional_t<(std::numeric_limits<superior_type>::digits < std::numeric_limits<std::int32_t>::digits), std::conditional_t<std::is_signed_v<superior_type>, std::int32_t, std::uint32_t>,
        std::conditional_t<(std::numeric_limits<superior_type>::digits < std::numeric_limits<std::int64_t>::digits), std::conditional_t<std::is_signed_v<superior_type>, std::int64_t, std::uint64_t>, double>>>>>;
};

template<typename T, typename U>
using promote_superior_type_t = typename promote_superior_type<T, U>::type;

Наконец, можно добавить функцию add<T, U>, шаг 3:

template<typename T, typename U, typename R = promote_superior_type_t<T, U>>
constexpr R add(T a, U b) {
    return static_cast<R>(a) + static_cast<R>(b);
}

Это все, что нужно. static_asserting для правильного ожидаемого вывода с учетом каждого возможного совпадения типов:

//8_t + U

auto add_i8_i8 =  add(std::int8_t(10),  std::int8_t(10));   //i8 + i8 -> i16
auto add_i8_u8 =  add(std::int8_t(10),  std::uint8_t(10));  //i8 + u8 -> u16
auto add_u8_u8 =  add(std::uint8_t(10), std::uint8_t(10));  //u8 + u8 -> u16
auto add_i8_i16 = add(std::int8_t(10),  std::int16_t(10));  //i8 + i16 -> i32
auto add_i8_u16 = add(std::int8_t(10),  std::uint16_t(10)); //i8 + u16 -> u32
auto add_u8_u16 = add(std::uint8_t(10), std::uint16_t(10)); //u8 + u16 -> u32
auto add_i8_i32 = add(std::int8_t(10),  std::int32_t(10));  //i8 + i32 -> i64
auto add_i8_u32 = add(std::int8_t(10),  std::uint32_t(10)); //i8 + u32 -> u64
auto add_u8_u32 = add(std::uint8_t(10), std::uint32_t(10)); //u8 + u32 -> u64
auto add_i8_i64 = add(std::int8_t(10),  std::int64_t(10));  //i8 + i64 -> d64
auto add_i8_u64 = add(std::int8_t(10),  std::uint64_t(10)); //i8 + u64 -> d64
auto add_u8_u64 = add(std::uint8_t(10), std::uint64_t(10)); //u8 + u64 -> d64
auto add_i8_f32 = add(std::int8_t(10),  float(10));         //i8 + f32 -> f32
auto add_u8_f32 = add(std::uint8_t(10), float(10));         //u8 + f32 -> f32
auto add_i8_d64 = add(std::int8_t(10),  double(10));        //i8 + d64 -> d64
auto add_u8_d64 = add(std::uint8_t(10), double(10));        //u8 + d64 -> d64

//16_t + U

auto add_i16_i16 = add(std::int16_t(10),  std::int16_t(10));  //i16 + i16 -> i32
auto add_i16_u16 = add(std::int16_t(10),  std::uint16_t(10)); //i16 + u16 -> u32
auto add_u16_u16 = add(std::uint16_t(10), std::uint16_t(10)); //u16 + u16 -> u32
auto add_i16_i32 = add(std::int16_t(10),  std::int32_t(10));  //i16 + i32 -> i64
auto add_i16_u32 = add(std::int16_t(10),  std::uint32_t(10)); //i16 + u32 -> u64
auto add_u16_u32 = add(std::uint16_t(10), std::uint32_t(10)); //u16 + u32 -> u64
auto add_i16_i64 = add(std::int16_t(10),  std::int64_t(10));  //i16 + i64 -> d64
auto add_i16_u64 = add(std::int16_t(10),  std::uint64_t(10)); //i16 + u64 -> d64
auto add_u16_u64 = add(std::uint16_t(10), std::uint64_t(10)); //u16 + u64 -> d64
auto add_i16_f32 = add(std::int16_t(10),  float(10));         //i16 + f32 -> f32
auto add_u16_f32 = add(std::uint16_t(10), float(10));         //u16 + f32 -> f32
auto add_i16_d64 = add(std::int16_t(10),  double(10));        //i16 + d64 -> d64
auto add_u16_d64 = add(std::uint16_t(10), double(10));        //u16 + d64 -> d64

//32_t + U

auto add_i32_i32 = add(std::int32_t(10),  std::int32_t(10));  //i32 + i32 -> i64
auto add_i32_u32 = add(std::int32_t(10),  std::uint32_t(10)); //i32 + u32 -> u64
auto add_u32_u32 = add(std::uint32_t(10), std::uint32_t(10)); //u32 + u32 -> u64
auto add_i32_i64 = add(std::int32_t(10),  std::int64_t(10));  //i32 + i64 -> d64
auto add_i32_u64 = add(std::int32_t(10),  std::uint64_t(10)); //i32 + u64 -> d64
auto add_u32_u64 = add(std::uint32_t(10), std::uint64_t(10)); //u32 + u64 -> d64 
auto add_i32_f32 = add(std::int32_t(10),  float(10));         //i32 + f32 -> f32
auto add_u32_f32 = add(std::uint32_t(10), float(10));         //u32 + f32 -> f32
auto add_i32_d64 = add(std::int32_t(10),  double(10));        //i32 + d64 -> d64
auto add_u32_d64 = add(std::uint32_t(10), double(10));        //u32 + d64 -> d64

//64_t + U

auto add_i64_i64 = add(std::int64_t(10),  std::int64_t(10));  //i64 + i64 -> d64
auto add_i64_u64 = add(std::int64_t(10),  std::uint64_t(10)); //i64 + u64 -> d64
auto add_u64_u64 = add(std::uint64_t(10), std::uint64_t(10)); //u64 + u64 -> d64
auto add_i64_f32 = add(std::int64_t(10),  float(10));         //i64 + f32 -> d64
auto add_u64_f32 = add(std::uint64_t(10), float(10));         //u64 + f32 -> d64
auto add_i64_d64 = add(std::int64_t(10),  double(10));        //i64 + d64 -> d64
auto add_u64_d64 = add(std::uint64_t(10), double(10));        //u64 + d64 -> d64


static_assert(std::is_same_v<decltype(add_i8_i8), std::int16_t>, "");
static_assert(std::is_same_v<decltype(add_i8_u8), std::uint16_t>, "");
static_assert(std::is_same_v<decltype(add_u8_u8), std::uint16_t>, "");
static_assert(std::is_same_v<decltype(add_i8_i16), std::int32_t>, "");
static_assert(std::is_same_v<decltype(add_i8_u16), std::uint32_t>, "");
static_assert(std::is_same_v<decltype(add_u8_u16), std::uint32_t>, "");
static_assert(std::is_same_v<decltype(add_i8_i32), std::int64_t>, "");
static_assert(std::is_same_v<decltype(add_i8_u32), std::uint64_t>, "");
static_assert(std::is_same_v<decltype(add_u8_u32), std::uint64_t>, "");
static_assert(std::is_same_v<decltype(add_i8_i64), double>, "");
static_assert(std::is_same_v<decltype(add_i8_u64), double>, "");
static_assert(std::is_same_v<decltype(add_u8_u64), double>, "");
static_assert(std::is_same_v<decltype(add_i8_f32), float>, "");
static_assert(std::is_same_v<decltype(add_u8_f32), float>, "");
static_assert(std::is_same_v<decltype(add_i8_d64), double>, "");
static_assert(std::is_same_v<decltype(add_u8_d64), double>, "");

static_assert(std::is_same_v<decltype(add_i16_i16), std::int32_t>, "");
static_assert(std::is_same_v<decltype(add_i16_u16), std::uint32_t>, "");
static_assert(std::is_same_v<decltype(add_u16_u16), std::uint32_t>, "");
static_assert(std::is_same_v<decltype(add_i16_i32), std::int64_t>, "");
static_assert(std::is_same_v<decltype(add_i16_u32), std::uint64_t>, "");
static_assert(std::is_same_v<decltype(add_u16_u32), std::uint64_t>, "");
static_assert(std::is_same_v<decltype(add_i16_i64), double>, "");
static_assert(std::is_same_v<decltype(add_i16_u64), double>, "");
static_assert(std::is_same_v<decltype(add_u16_u64), double>, "");
static_assert(std::is_same_v<decltype(add_i16_f32), float>, "");
static_assert(std::is_same_v<decltype(add_u16_f32), float>, "");
static_assert(std::is_same_v<decltype(add_i16_d64), double>, "");
static_assert(std::is_same_v<decltype(add_u16_d64), double>, "");

static_assert(std::is_same_v<decltype(add_i32_i32), std::int64_t>, "");
static_assert(std::is_same_v<decltype(add_i32_u32), std::uint64_t>, "");
static_assert(std::is_same_v<decltype(add_u32_u32), std::uint64_t>, "");
static_assert(std::is_same_v<decltype(add_i32_i64), double>, "");
static_assert(std::is_same_v<decltype(add_i32_u64), double>, "");
static_assert(std::is_same_v<decltype(add_u32_u64), double>, "");
static_assert(std::is_same_v<decltype(add_i32_f32), float>, "");
static_assert(std::is_same_v<decltype(add_u32_f32), float>, "");
static_assert(std::is_same_v<decltype(add_i32_d64), double>, "");
static_assert(std::is_same_v<decltype(add_u32_d64), double>, "");

static_assert(std::is_same_v<decltype(add_i64_i64), double>, "");
static_assert(std::is_same_v<decltype(add_i64_u64), double>, "");
static_assert(std::is_same_v<decltype(add_u64_u64), double>, "");
static_assert(std::is_same_v<decltype(add_i64_f32), double>, "");
static_assert(std::is_same_v<decltype(add_u64_f32), double>, "");
static_assert(std::is_same_v<decltype(add_i64_d64), double>, "");
static_assert(std::is_same_v<decltype(add_u64_d64), double>, "");

Есть одна неопределенность:

i64 + f32 -> d64
u64 + f32 -> d64
//...
static_assert(std::is_same_v<decltype(add_i64_f32), double>, "");
static_assert(std::is_same_v<decltype(add_u64_f32), double>, "");

в соответствии с вашими правилами также может быть:

i64 + f32 -> f32 
u64 + f32 -> f32 
//...
static_assert(std::is_same_v<decltype(add_i64_f32), float>, "");
static_assert(std::is_same_v<decltype(add_u64_f32), float>, "");

полный код:

#include <type_traits>
#include <limits>
#include <cstdint>

template<typename T, typename U>
struct larger_arithmetic_type {
    static_assert(std::is_arithmetic_v<T>, "T must be arithmetic");
    static_assert(std::is_arithmetic_v<U>, "U must be arithmetic");
    using type = typename std::conditional_t<(std::numeric_limits<T>::digits < std::numeric_limits<U>::digits), U, T>;
};

template<typename T, typename U>
using larger_arithmetic_type_t = typename larger_arithmetic_type<T, U>::type;

template<typename T, typename U>
struct arithmetic_superior_type {
    using type = typename 
        std::conditional_t<std::is_floating_point_v<T> && std::is_floating_point_v<U>, larger_arithmetic_type_t<T, U>,
        std::conditional_t<std::is_floating_point_v<T>, T, 
        std::conditional_t<std::is_floating_point_v<U>, U, 
        larger_arithmetic_type_t<T, U>>>>;

};

template<typename T, typename U>
using arithmetic_superior_type_t = typename arithmetic_superior_type<T, U>::type;


template<typename T, typename U>
struct promote_superior_type {
    using superior_type = arithmetic_superior_type_t<T, U>;

    using type = typename
        std::conditional_t<(sizeof(T) == 8u || sizeof(U) == 8u), double, 
        std::conditional_t<std::is_floating_point_v<superior_type>, superior_type, 
        std::conditional_t<(std::numeric_limits<superior_type>::digits < std::numeric_limits<std::int16_t>::digits), std::conditional_t<std::is_signed_v<superior_type>, std::int16_t, std::uint16_t>,
        std::conditional_t<(std::numeric_limits<superior_type>::digits < std::numeric_limits<std::int32_t>::digits), std::conditional_t<std::is_signed_v<superior_type>, std::int32_t, std::uint32_t>,
        std::conditional_t<(std::numeric_limits<superior_type>::digits < std::numeric_limits<std::int64_t>::digits), std::conditional_t<std::is_signed_v<superior_type>, std::int64_t, std::uint64_t>, double>>>>>;
};

template<typename T, typename U>
using promote_superior_type_t = typename promote_superior_type<T, U>::type;

template<typename T, typename U, typename R = promote_superior_type_t<T, U>>
constexpr R add(T a, U b) {
    return static_cast<R>(a) + static_cast<R>(b);
}


int main() {
    arithmetic_superior_type_t<std::int32_t, float> a;          //-> float
    arithmetic_superior_type_t<std::uint32_t, std::int32_t> b;  //-> std::uint32_t
    arithmetic_superior_type_t<std::uint32_t, std::uint32_t> c; //-> std::uint32_t
    arithmetic_superior_type_t<std::uint64_t, std::uint32_t> d; //-> std::uint64_t
    arithmetic_superior_type_t<float, double> e;                //-> double
    arithmetic_superior_type_t<std::uint16_t, std::int64_t> f;  //-> std::int64_t


    //8_t + U

    auto add_i8_i8 =  add(std::int8_t(10),  std::int8_t(10));   //i8 + i8 -> i16
    auto add_i8_u8 =  add(std::int8_t(10),  std::uint8_t(10));  //i8 + u8 -> u16
    auto add_u8_u8 =  add(std::uint8_t(10), std::uint8_t(10));  //u8 + u8 -> u16
    auto add_i8_i16 = add(std::int8_t(10),  std::int16_t(10));  //i8 + i16 -> i32
    auto add_i8_u16 = add(std::int8_t(10),  std::uint16_t(10)); //i8 + u16 -> u32
    auto add_u8_u16 = add(std::uint8_t(10), std::uint16_t(10)); //u8 + u16 -> u32
    auto add_i8_i32 = add(std::int8_t(10),  std::int32_t(10));  //i8 + i32 -> i64
    auto add_i8_u32 = add(std::int8_t(10),  std::uint32_t(10)); //i8 + u32 -> u64
    auto add_u8_u32 = add(std::uint8_t(10), std::uint32_t(10)); //u8 + u32 -> u64
    auto add_i8_i64 = add(std::int8_t(10),  std::int64_t(10));  //i8 + i64 -> d64
    auto add_i8_u64 = add(std::int8_t(10),  std::uint64_t(10)); //i8 + u64 -> d64
    auto add_u8_u64 = add(std::uint8_t(10), std::uint64_t(10)); //u8 + u64 -> d64
    auto add_i8_f32 = add(std::int8_t(10),  float(10));         //i8 + f32 -> f32
    auto add_u8_f32 = add(std::uint8_t(10), float(10));         //u8 + f32 -> f32
    auto add_i8_d64 = add(std::int8_t(10),  double(10));        //i8 + d64 -> d64
    auto add_u8_d64 = add(std::uint8_t(10), double(10));        //u8 + d64 -> d64

    //16_t + U

    auto add_i16_i16 = add(std::int16_t(10),  std::int16_t(10));  //i16 + i16 -> i32
    auto add_i16_u16 = add(std::int16_t(10),  std::uint16_t(10)); //i16 + u16 -> u32
    auto add_u16_u16 = add(std::uint16_t(10), std::uint16_t(10)); //u16 + u16 -> u32
    auto add_i16_i32 = add(std::int16_t(10),  std::int32_t(10));  //i16 + i32 -> i64
    auto add_i16_u32 = add(std::int16_t(10),  std::uint32_t(10)); //i16 + u32 -> u64
    auto add_u16_u32 = add(std::uint16_t(10), std::uint32_t(10)); //u16 + u32 -> u64
    auto add_i16_i64 = add(std::int16_t(10),  std::int64_t(10));  //i16 + i64 -> d64
    auto add_i16_u64 = add(std::int16_t(10),  std::uint64_t(10)); //i16 + u64 -> d64
    auto add_u16_u64 = add(std::uint16_t(10), std::uint64_t(10)); //u16 + u64 -> d64
    auto add_i16_f32 = add(std::int16_t(10),  float(10));         //i16 + f32 -> f32
    auto add_u16_f32 = add(std::uint16_t(10), float(10));         //u16 + f32 -> f32
    auto add_i16_d64 = add(std::int16_t(10),  double(10));        //i16 + d64 -> d64
    auto add_u16_d64 = add(std::uint16_t(10), double(10));        //u16 + d64 -> d64

    //32_t + U

    auto add_i32_i32 = add(std::int32_t(10),  std::int32_t(10));  //i32 + i32 -> i64
    auto add_i32_u32 = add(std::int32_t(10),  std::uint32_t(10)); //i32 + u32 -> u64
    auto add_u32_u32 = add(std::uint32_t(10), std::uint32_t(10)); //u32 + u32 -> u64
    auto add_i32_i64 = add(std::int32_t(10),  std::int64_t(10));  //i32 + i64 -> d64
    auto add_i32_u64 = add(std::int32_t(10),  std::uint64_t(10)); //i32 + u64 -> d64
    auto add_u32_u64 = add(std::uint32_t(10), std::uint64_t(10)); //u32 + u64 -> d64 
    auto add_i32_f32 = add(std::int32_t(10),  float(10));         //i32 + f32 -> f32
    auto add_u32_f32 = add(std::uint32_t(10), float(10));         //u32 + f32 -> f32
    auto add_i32_d64 = add(std::int32_t(10),  double(10));        //i32 + d64 -> d64
    auto add_u32_d64 = add(std::uint32_t(10), double(10));        //u32 + d64 -> d64

    //64_t + U

    auto add_i64_i64 = add(std::int64_t(10),  std::int64_t(10));  //i64 + i64 -> d64
    auto add_i64_u64 = add(std::int64_t(10),  std::uint64_t(10)); //i64 + u64 -> d64
    auto add_u64_u64 = add(std::uint64_t(10), std::uint64_t(10)); //u64 + u64 -> d64
    auto add_i64_f32 = add(std::int64_t(10),  float(10));         //i64 + f32 -> d64
    auto add_u64_f32 = add(std::uint64_t(10), float(10));         //u64 + f32 -> d64
    auto add_i64_d64 = add(std::int64_t(10),  double(10));        //i64 + d64 -> d64
    auto add_u64_d64 = add(std::uint64_t(10), double(10));        //u64 + d64 -> d64


    static_assert(std::is_same_v<decltype(add_i8_i8), std::int16_t>, "");
    static_assert(std::is_same_v<decltype(add_i8_u8), std::uint16_t>, "");
    static_assert(std::is_same_v<decltype(add_u8_u8), std::uint16_t>, "");
    static_assert(std::is_same_v<decltype(add_i8_i16), std::int32_t>, "");
    static_assert(std::is_same_v<decltype(add_i8_u16), std::uint32_t>, "");
    static_assert(std::is_same_v<decltype(add_u8_u16), std::uint32_t>, "");
    static_assert(std::is_same_v<decltype(add_i8_i32), std::int64_t>, "");
    static_assert(std::is_same_v<decltype(add_i8_u32), std::uint64_t>, "");
    static_assert(std::is_same_v<decltype(add_u8_u32), std::uint64_t>, "");
    static_assert(std::is_same_v<decltype(add_i8_i64), double>, "");
    static_assert(std::is_same_v<decltype(add_i8_u64), double>, "");
    static_assert(std::is_same_v<decltype(add_u8_u64), double>, "");
    static_assert(std::is_same_v<decltype(add_i8_f32), float>, "");
    static_assert(std::is_same_v<decltype(add_u8_f32), float>, "");
    static_assert(std::is_same_v<decltype(add_i8_d64), double>, "");
    static_assert(std::is_same_v<decltype(add_u8_d64), double>, "");

    static_assert(std::is_same_v<decltype(add_i16_i16), std::int32_t>, "");
    static_assert(std::is_same_v<decltype(add_i16_u16), std::uint32_t>, "");
    static_assert(std::is_same_v<decltype(add_u16_u16), std::uint32_t>, "");
    static_assert(std::is_same_v<decltype(add_i16_i32), std::int64_t>, "");
    static_assert(std::is_same_v<decltype(add_i16_u32), std::uint64_t>, "");
    static_assert(std::is_same_v<decltype(add_u16_u32), std::uint64_t>, "");
    static_assert(std::is_same_v<decltype(add_i16_i64), double>, "");
    static_assert(std::is_same_v<decltype(add_i16_u64), double>, "");
    static_assert(std::is_same_v<decltype(add_u16_u64), double>, "");
    static_assert(std::is_same_v<decltype(add_i16_f32), float>, "");
    static_assert(std::is_same_v<decltype(add_u16_f32), float>, "");
    static_assert(std::is_same_v<decltype(add_i16_d64), double>, "");
    static_assert(std::is_same_v<decltype(add_u16_d64), double>, "");

    static_assert(std::is_same_v<decltype(add_i32_i32), std::int64_t>, "");
    static_assert(std::is_same_v<decltype(add_i32_u32), std::uint64_t>, "");
    static_assert(std::is_same_v<decltype(add_u32_u32), std::uint64_t>, "");
    static_assert(std::is_same_v<decltype(add_i32_i64), double>, "");
    static_assert(std::is_same_v<decltype(add_i32_u64), double>, "");
    static_assert(std::is_same_v<decltype(add_u32_u64), double>, "");
    static_assert(std::is_same_v<decltype(add_i32_f32), float>, "");
    static_assert(std::is_same_v<decltype(add_u32_f32), float>, "");
    static_assert(std::is_same_v<decltype(add_i32_d64), double>, "");
    static_assert(std::is_same_v<decltype(add_u32_d64), double>, "");

    static_assert(std::is_same_v<decltype(add_i64_i64), double>, "");
    static_assert(std::is_same_v<decltype(add_i64_u64), double>, "");
    static_assert(std::is_same_v<decltype(add_u64_u64), double>, "");
    static_assert(std::is_same_v<decltype(add_i64_f32), double>, "");
    static_assert(std::is_same_v<decltype(add_u64_f32), double>, "");
    static_assert(std::is_same_v<decltype(add_i64_d64), double>, "");
    static_assert(std::is_same_v<decltype(add_u64_d64), double>, "");
}
...