0

We have a big C++ project where we rely on compiler warnings and Flexelint to identify potential programming errors. I was curious about how they will warn us once we accidentally try to cast an enum value to a narrower integer.

As suggested by Lint, we usually perform static casts from the enum to the integer. Lint doesn't like implicit casts. We usually cast to the exact type expected by the method.

I got interesting results, see this experiment:

#include <iostream>
#include <string>
#include <stdint.h>

void foo(uint8_t arg)
{
    std::cout << static_cast<int>(arg) << std::endl;
}

enum Numbers
{
    hundred  =  100,
    thousand = 1000,
};

int main()
{
    std::cout << "start" << std::endl;
    foo(static_cast<uint8_t>(hundred));   // 1) no compiler or lint warning
    foo(static_cast<uint8_t>(thousand));  // 2) no compiler or lint warning
    foo(static_cast<int>(thousand));      // 3) compiler and lint warning
    foo(thousand);                        // 4) compiler and lint warning
    std::cout << "end" << std::endl;
}

http://cpp.sh/5hpyz

First case is not a concern, just to mention the good case.

Interestingly, I only got compiler warnings in the latter two cases, when doing an implicit cast. The explicit cast in case 2) will truncate the value (output is 232 like the following two) but with no warning. Ok, the compiler is probably assuming I know what I'm doing here with my explicit cast to uint8_t. Fair enough.

I expected Lint to help me out here. I run this code in Gimpel's online Lint but didn't get any warnings either. Only in the latter two cases again, with this warning:

warning 569: loss of information (call) in implicit conversion from 'int' 1000 (10 bits) to 'uint8_t' (aka 'unsigned char') (8 bits)

Again, the explicit cast to uint8_t in case 2), that truncates my value, doesn't bother Lint at all.

Given a case where all values in an enum fit into the uint8_t. But in some future, we add bigger values (or say: more than 256 values in total), cast them and without noticing that this will truncate them and get unexpected results.

By default, I always cast to the target variable size (case 2) ). Given this experiment, I wonder if this is a wise approach. Shall I cast to the widest type and rely on implicit casts instead (case 3) )?

What's the right approach to get the expected warnings?

craesh
  • 3,665
  • 3
  • 23
  • 29
  • 2
    a cast is how you tell the compiler "I know what I'm doing" so it won't emit any warnings – phuclv Aug 04 '21 at 07:58

2 Answers2

2

You could also write foo(uint8_t{thousand}); instead of a static_cast. With that, you would get a compiler error/warning if thousand is too large for uint8_t. But I don't know what lint thinks about it

Fomas
  • 114
  • 4
1

This is a problem I also encountered. What I found to work best is to write a function which performs the cast for you and would generate an error in case something is wrong based on type traits.

#include <type_traits>
#include <limits>

template<class TYPE>
TYPE safe_cast(const Numbers& number)
{
   using FROM_TYPE = std::underlying_type_t<Numbers>;
   // Might have to add some additional code here to fix signed unsigned comparisons. 
   if((abs(std::numeric_limits<TYPE>::min()) > static_cast<FROM_TYPE>(number)) ||
      (std::numeric_limits<TYPE>::max() < static_cast<FROM_TYPE>(number)))
   {
     // Throw an error or assert.
     std::cout << "Error in safe_cast" << std::endl;
   }
      
   return static_cast<TYPE>(number);  
}

Hope this wil help

p.s. In case you could rewrite this to compile time with a constexpr you could also uses static_assert.

Bart
  • 1,405
  • 6
  • 32
  • checking range like that is fragile. For example if `TYPE` and `FROM_TYPE` differ in signness then it'll break. [Use `std::in_range` instead](https://stackoverflow.com/a/68604121/995714) – phuclv Aug 06 '21 at 02:23
  • @phuclv That is true, I had no time to also include that code but did include the comment. – Bart Aug 06 '21 at 05:56