6

msvc's code analyzer for the cpp core guidelines tells me

Warning C26472 Don't use a static_cast for arithmetic conversions. Use brace initialization, gsl::narrow_cast or gsl::narrow (type.1).

for this snippet

static_cast<IntType>(static_cast<unsigned long long>(hexValue(digit)) << (digitIdx * 4));

Why shouldn't I use static_cast here?

Also, with brace init this would look like this

IntType{unsigned long long{hexValue(digit)} << (digitIdx * 4)};

which doesn't look any better imo. This looks more like a function style cast than anything else.

I cannot use gsl and I think gsl::narrow is a wrapper around static_cast itself, so is this purely a readability issue here?

3 Answers 3

5

so is this purely a readability issue here?

Nope. Brace initialization prohibits narrowing conversions, and will cause a diagnostic. The guideline's purpose is to help protect code from unintended narrowing. A static_cast will allow a narrowing conversion through silently.

You seem to be dealing in integers, so short of using an extended integer type that is larger than unsigned long long, you'll probably not hit any narrowing.

But the guideline is there for the general case, and it's better to write code consistently, even when there is no actual risk.

Sign up to request clarification or add additional context in comments.

2 Comments

IntType is a template so there will almost always be narrowing in the left static_cast won't there (i.e. when IntType != unsigned long long)?
@Timo - Ah, you are correct. I confess I got fixated on the inner static_cast. Yes you are correct, this will usually cause narrowing. gsl::narrow_cast exists for this cases. It makes the potential for narrowing stand out (which is why the diagnostic is silenced). If you can't use it then you only have static_cast available. Disabling the warning locally, and a comment will probably do if you can't pull in gsl.
3

Here is a link to Microsoft documentation:

https://learn.microsoft.com/en-us/cpp/code-quality/c26472?view=vs-2019

  • gsl::narrow ensures lossless conversion and throws gsl::narrowing_error if it's not possible.
  • gsl::narrow_cast clearly states that conversion can lose data and it is acceptable.

static_cast doesn't perform these checks, so it is safer to explicitly state your intentions.

Comments

2

Warning C26472: Don't use a static_cast for arithmetic conversions. Use brace initialization, gsl::narrow_cast or gsl::narrow (type.1).

The advice "Don't use static_cast for arithmetic types" is a strict version of Avoid casts and prefer named casts (said in type.1).

My advice is that, in order to keep type safety strictly, static_cast should be not used for arithmetic conversions. However, in many occasions, the chance of unsafe type cast is very low or type safety can be well ensured by algorithms and programs, static_cast could be used for arithmetic conversions.


More thinking about type safety of arithmetic types is giving as following:

Brace initialization is an ideal type-safe strategy as it prohibited narrowing conversions. For gsl::narrow and gsl::narrow_cast, there are no similar narrow conversion functions in C++ Standard Library at present.

  • gsl::narrow ensures lossless conversion and throws gsl::narrowing_error if it's not possible.
  • gsl::narrow_cast clearly states that conversion can lose data and it's acceptable.

According the document of GSL, gsl::narrow_cast is a named cast that is identical to a static_cast. Thus, it would still cause unsafe narrow conversion as it does not check the value it is converting (same as static_cast). For example,

std::uint8_t c1 = gsl::narrow_cast<std::uint8_t>(1000.5);// same as using static_cast
std::uint8_t c2 = gsl::narrow_cast<std::uint8_t>(-1.5); // same as using static_cast

The above code can be run successfully, while the narrow conversion is still unsafe. If you do not want to use GSL and do not care too much about performance, arithmetic conversions could also be implemented by writing a simpler type-safe numeric_cast function as giving in the following code.

#include <iostream>
#include <limits>
void error(const std::string& errormessage){
    std::cerr << errormessage;
    throw std::runtime_error(errormessage);
}

template <typename Target, typename Source>
constexpr Target numeric_cast(Source&& src){    
    if (typeid(Target) != typeid(bool) &&
        (src < std::numeric_limits<Target>::lowest() ||
         src > std::numeric_limits<Target>::max())  )
        error("Unsafe numerical cast.\n");
    return static_cast<Target>(std::forward<Source>(src));
}

int main() {    
    //1. Numerical promotion
    float f1{ 1.1f };   double dd1{ f1 }; //Suggested!
    double d1{ numeric_cast<double>(f1) }; //Ok
    
    //2. Numerical narrowing
    double dd2{ 1.1 }; float f2 = numeric_cast<float>(dd2); //OK 
    std::size_t uu2{ 10 }; std::uint8_t u2 = numeric_cast<std::uint8_t>(uu2);  //OK 

    //3. Numerical truncating
    double d3{ 1.5 }; std::uint8_t u3 = numeric_cast<std::uint8_t>(d3);  //OK 

    // Errors detecked
    //double d4{ -1.5 }; std::uint8_t u4 = numeric_cast<std::uint8_t>(d4); //error detecked
    //double dd4{ 1000 }; std::uint8_t uu4 = numeric_cast<std::uint8_t>(dd4); //error detecked   
}

The numeric_cast function could be used for numerical promotion, numerical narrowing and numerical truncating simultaneously. It can also find the errors of unreasonable arithmetic conversions by check the value it is converting and the limits of converting target.

Of course, to keep typy-safety, the performance of numeric_cast is relatively lower when compared with other arithmetic conversions without value check.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.