c++narrowing

c++ proper way to downcast int types


First, suppose I have

int16_t a;
int8_t b;

. I wish to do b=a; by discarding all the MSB, including sign bit, of a that doesn't fit in b. What would be a proper way to do this?

I can't really find any useful, recent information on this topic. This link says that signed narrowing conversion is compiler-dependent, but it's in C not C++. I also don't want compiler-dependent behaviors. Here says that narrowing conversion should be an error. I'm pretty confused on what to do.

Second, if both a and b were their unsigned counterpart, then is b=a; a proper approach? According to the first link, this is defined at least in C.


Solution

  • From C++20 onward, you can simply do b=a; and get the correct behavior. The = operator performs implicit conversion to the type of its left operand [expr.ass p3 from standard draft N4860]. Implicit conversion includes integral conversions [conv p1.2]. Integral conversion in this case results in:

    the unique value of the destination type that is congruent to the source integer modulo 2^N, where N is the width of the destination type. [conv.integral p3].

    Mathematically, this is exactly the effect of truncating the high bits, and compilers implement it with a simple truncation.

    You may if you wish use an explicit static cast:

    b = static_cast<int8_t>(a);
    

    This may avoid warnings from some compilers; the actual effect is the same.

    The note you found about narrowing conversions being forbidden is applicable only to list initialization, e.g. int8_t b{a};. It is not relevant to simple assignment, which does narrowing conversions just fine.


    Prior to C++20, integer conversion caused implementation-defined behavior if the value being converted was out of range for the destination type. As far as I know, all mainstream compilers on mainstream platforms defined the behavior to be truncation, exactly as was codified by C++20, so in practice b=a; will do the right thing on most pre-C++20 implementations as well.


    If that's not good enough for you, then you can begin by converting the value to the unsigned type uint8_t, whose behavior for out-of-range values has always been truncation, back to at least C++98. Then do some arithmetic, which hopefully is optimized out, to ensure that you assign to b a value that is in range:

    uint8_t tmp = a;
    if (tmp >= 128)
        b = tmp - 256;
    else
        b = tmp;
    

    Try on godbolt and notice that indeed the temporary variable is optimized out and no subtraction is done; it optimizes to a 16-bit load and an 8-bit store.