How to make small signed integer literals that take into account 2s complement?

111 Views Asked by At

I've been trying to make signed literal short hands for <cstdint> types, for example, u8, u16, etc...

I developed a function like this:

constexpr std::int8_t operator "" _i8(unsigned long long int value){
    unsigned long long int mask = 0xEF;
    if(value && mask == value){
        return static_cast<std::int8_t>(value);
    }else{
        throw std::out_of_range("");
    }
}

I then looked up and found that - is actually not part of the literal. What this means for me is that the above code( if it included 128, right now it doesn't) wouldn't actually work correctly for -128.

When I try this out in godbolt

#include <type_traits> 
#include <iostream> 
#include <cstdint> 
#include <bitset> 


int main(){

    std::cout << std::bitset<8>(static_cast<std::int8_t>(128)) << std::endl; 
    std::cout << static_cast<std::int32_t>(static_cast<std::int8_t>(128)) << std::endl; 
    std::cout << static_cast<std::int32_t>(static_cast<std::int8_t>(-128)) << std::endl; 
    std::cout << static_cast<std::int32_t>(-static_cast<std::int8_t>(128)) << std::endl; 
    return 0; 
}

I get 10000000, -128, -128, and 128

Technically using 128 could work in the above example, it would just force the value to be -128 on the output, but when they (correctly) add a negative, it will turn into 128, and not be a int8_t. I'd like errors on 128 with no negative, and no errors on 128 with negative.

I thought about making some sort of temporary object with - overloaded for it, and implicitly converting, but even with that, it won't work properly in template functions, so the unary - operator would have to return the base type, and I would somehow have to return the base type switched on if the value is 128 or not, but I don't think that's possible.

I basically want the following to work:

template<typename T> 
foo(const T& bar, const T& baz); 

foo(127_i8, std::int8_t(32));
foo(-128_i8, std::int8_t(32));

and this to not work:

foo(128_i8, std::int8_t(32));
1

There are 1 best solutions below

0
303 On

Even for C++20, where signed integers are now required to be two's complement, this is simply not possible as there is no such thing as negative integer literals. This means that mimicking a negative integer literal will always be a two-step process. There is no way to create a -128_i8 without creating a 128_i8 first, and at that point (the first step), there is no knowing if some kind of negation operation will be applied to it later.

So, while we could do something like this to make static_assert(-128_i8 == -128); compile successfully:

#include <cstdint>

struct [[nodiscard]] i8 {
    std::int8_t n;

    [[nodiscard]]
    constexpr auto operator-() const noexcept -> i8
    { return {static_cast<decltype(n)>(-n)}; }

    [[nodiscard]]
    constexpr operator decltype(n)() const noexcept
    { return n; }
};

[[nodiscard]]
constexpr auto operator"" _i8(unsigned long long int n) noexcept -> i8
{ return {static_cast<decltype(i8::n)>(n)}; }

// note: this example assumes that the implementation defined
//   truncation does the right thing
static_assert(-128_i8 == -128);
static_assert((128_i8).operator-() == -128);

There is still no way of discerning 128_i8 from -128_i8 at the point of creation of the user-defined literal. The best we can do here is to match the truncation behavior of the underlying integer type, as in:

static_assert(-128_i8 == 128_i8);
static_assert(static_cast<std::int8_t>(-128) == static_cast<std::int8_t>(128));