Metamorphosis frog Meyers

C++ Core Guidelines: Rules for Conversions and Casts

What has narrowing conversion and casts in common? They are often the source of errors; therefore, I will write about them today.

 

Metamorphosis frog Meyers

Here are the rules from the guidelines.

Narrowing conversion is a conversion of a value, including the loss of its precision. Most of the time that is not what you want.

ES.46: Avoid narrowing conversions

Here are a few examples from the guidelines.

 

Rainer D 6 P2 500x500Modernes C++ Mentoring

Be part of my mentoring programs:

  • "Fundamentals for C++ Professionals" (open)
  • "Design Patterns and Architectural Patterns with C++" (open)
  • "C++20: Get the Details" (open)
  • "Concurrency with Modern C++" (starts March 2024)
  • Do you want to stay informed: Subscribe.

     

    double d = 7.9;
    int i = d;    // bad: narrowing: i becomes 7
    i = (int) d;  // bad: we're going to claim this is still not explicit enough
    
    void f(int x, long y, double d)
    {
        char c1 = x;   // bad: narrowing
        char c2 = y;   // bad: narrowing
        char c3 = d;   // bad: narrowing
    }
    

     

    If you want a narrowing conversion, you should do it explicitly, not implicitly, according to the Python rule from The Zen of PythonExplicit is better than implicit. The guideline support library (GSL) has two casts to express your intent: gsl::narrow_cast and gsl::narrow.

    double d = 7.9;
    i = narrow_cast<int>(d);   // OK (you asked for it): narrowing: i becomes 7
    i = narrow<int>(d);        // OK: throws narrowing_error
    

     

    The gsl::narrow_cast performs the cast, and the gsl::narrow cast throws an exception if a narrowing conversion happens.

    Most of the time, a narrowing conversion happened secretly. How can you protect yourself from this? Use the power of the curly braces:

    // suppressNarrowingConversion.cpp
    
    void f(int x, long y, double d){
        char c1 = {x};   
        char c2 = {y};   
        char c3 = {d};   
    }
    
    int main(){
    
      double d = {7.9};         
      int i = {d};    
    
      f(3, 3l, 3.0);
    }
    

     

    All initializations are put into curly braces. According to the C++11 standard, the compiler has to warn you if a narrowing conversion happens.

     narrowingConversion

    Explicit is better than implicit. This will not hold a C-cast.

    ES.48: Avoid casts

    Let’s see what will happen if we screw up the type of system.

    // casts.cpp
    
    #include <iostream>
    
    int main(){
    
      double d = 2;
      auto p = (long*)&d;
      auto q = (long long*)&d;
      std::cout << d << ' ' << *p << ' ' << *q << '\n';
      
    }
    

     

    Neither the result with the Visual Studio compiler

    castsWin

    nor the result with the GCC or the clang compiler is promising.

     

    castsGccClang

     

    What is bad about the C-cast? You don’t see which cast is performed. If you perform a C-cast, a combination of casts will be applied if necessary. Roughly speaking, a C-cast starts with a static_cast, continues with a const_cast, and finally performs a reinterpret_cast.

    Of course, you know how I will continue: explicit is better than implicit.

    ES.49: If you must use a cast, use a named cast

    Including the GSL, C++ offers eight different named casts. Here are including a short description:

    • static_cast: conversion between similar types such as pointer types or numeric types
    • const_cast: adds or removes const or volatile 
    • reinterpret_cast: converts between pointers or between integral types and pointers
    • dynamic_ cast: converts between polymorph pointers or references in the same class hierarchy
    • std::move: converts to an rvalue reference
    • std::forward: converts to an rvalue reference
    • gsl::narrow_cast: applies a static_cast
    • gsl::narrow: applies a static_cast

    What? std::move and std::forward are casts? Le’s have a closer look at the internals of std::move: 

    static_cast<std::remove_reference<decltype(arg)>::type&&>(arg)
    

     

    First, the type of argument arg is determined by decltype(arg). Then all references are removed, and two new references are added. The function std::remove_reference is from the type-traits library. I have already written a few posts to the type-traits library.  In the end, we will always get an rvalue reference. 

    Casting away const is undefined behavior.

    ES.50: Don’t cast away const

    Let me be more specific. Casting away const is undefined behavior if the underlying object, such as constInt is not mutable.

     

    const int constInt = 10;
    const int* pToConstInt = &constInt;
     
    int* pToInt = const_cast<int*>(pToConstInt);
    *pToInt = 12;          // undefined behaviour
    

     

    If you don’t believe me, there is a footnote in the C standard [ISO/IEC 9899:2011] (subclause 6.7.3, paragraph 4) relevant to the C++ standard: The implementation may place a const object that is not volatile in a read-only region of storage. Moreover, the implementation need not allocate storage for such an object if its address is never used.

    Did I mention mutable? mutable is one of the unknown features in C++. mutable allows you to differentiate between bitwise and logical constness. What? 

    Imagine you want to implement the interface to a telephone book. For simplicity reasons, the entries should be in a std::unordered_map.

     

    // teleBook.cpp

    #include <iostream>
    #include <string> #include <unordered_map> std::unordered_map<std::string, int> getUpdatedTelephoneBook(){ // generate a new, updated telephone book return {{"grimm",123}, {"huber", 456}, {"schmidt", 321}}; } class TelephoneBook{ public: int getNumber(const std::string& name) const { auto ent = cache.find(name); if(ent != cache.end()){ return ent->second; } else{ cache = getUpdatedTelephoneBook(); // (2) return cache[name]; } } private: // (1) std::unordered_map<std::string, int> cache = {{"grimm",123}, {"huber", 456}}; }; int main(){ std::cout << std::endl; TelephoneBook telBook; // (3) std::cout << "grimm " << telBook.getNumber("grimm") << std::endl; std::cout << "schmidt " << telBook.getNumber("schmidt") << std::endl; std::cout << std::endl; }

     

    My telephone book (1) is tiny. Usually, a telephone book is quite big, and updating it is quite expensive (2). This means updating a printed telephone book will happen only once a year in Germany. From the conceptional view, the inquiries to the teleBook (3) should be const. This is not possible because the unordered_map is modified in the method getNumber. Here is the proof in red ellipses.

    telephoneBookError 

    The qualifier mutable allows you to differentiate between bitwise and logical constness. The telBook is logical but not bitwise const. 

    // teleBook.cpp

    #include <iostream>
    #include <string> #include <unordered_map> std::unordered_map<std::string, int> getUpdatedTelephoneBook(){ // generate a new, updated telephone book return {{"grimm",123}, {"huber", 456}, {"schmidt", 321}}; } class TelephoneBook{ public: int getNumber(const std::string& name) const { auto ent = cache.find(name); if(ent != cache.end()){ return ent->second; } else{ cache = getUpdatedTelephoneBook(); // (2) return cache[name]; } } private: // (1) mutable std::unordered_map<std::string, int> cache = {{"grimm",123}, {"huber", 456}}; }; int main(){ std::cout << std::endl; const TelephoneBook telBook; // (3) std::cout << "grimm " << telBook.getNumber("grimm") << std::endl; std::cout << "schmidt " << telBook.getNumber("schmidt") << std::endl; std::cout << std::endl; }

     

    I just added const (3) to the telBook and mutable to the cache (1), and the program behaves as expected.

    telephoneBook 

    ES.55: Avoid the need for range checking

    I can make it short. By using the range-based for-loop or algorithms of the STL, there is no need to check the range.

     

    std::array<int, 10> arr = {5, 7, 4, 2, 8, 6, 1, 9, 0, 3}; 
    std::sort(arr.begin(), arr.end());
    for (auto a : arr) {
        std::cout << a << " ";
    }   
    // 0 1 2 3 4 5 6 7 8 9
    

     

    What’s next?

    In the next post on expressions, I will write about std::move, new and delete, and slicing. Slicing is probably one of the darkest corners of C++. So, stay tuned.

     

     

    Thanks a lot to my Patreon Supporters: Matt Braun, Roman Postanciuc, Tobias Zindl, G Prvulovic, Reinhold Dröge, Abernitzke, Frank Grimm, Sakib, Broeserl, António Pina, Sergey Agafyin, Андрей Бурмистров, Jake, GS, Lawton Shoemake, Jozo Leko, John Breland, Venkat Nandam, Jose Francisco, Douglas Tinkham, Kuchlong Kuchlong, Robert Blanch, Truels Wissneth, Kris Kafka, Mario Luoni, Friedrich Huber, lennonli, Pramod Tikare Muralidhara, Peter Ware, Daniel Hufschläger, Alessandro Pezzato, Bob Perry, Satish Vangipuram, Andi Ireland, Richard Ohnemus, Michael Dunsky, Leo Goodstadt, John Wiederhirn, Yacob Cohen-Arazi, Florian Tischler, Robin Furness, Michael Young, Holger Detering, Bernd Mühlhaus, Stephen Kelley, Kyle Dean, Tusar Palauri, Dmitry Farberov, Juan Dent, George Liao, Daniel Ceperley, Jon T Hess, Stephen Totten, Wolfgang Fütterer, Matthias Grün, Phillip Diekmann, Ben Atakora, Ann Shatoff, Rob North, Bhavith C Achar, Marco Parri Empoli, moon, Philipp Lenk, Hobsbawm, and Charles-Jianye Chen.

    Thanks, in particular, to Jon Hess, Lakshman, Christian Wittenhorst, Sherhy Pyton, Dendi Suhubdy, Sudhakar Belagurusamy, Richard Sargeant, Rusty Fleming, John Nebel, Mipko, Alicja Kaminska, Slavko Radman, and David Poole.

    My special thanks to Embarcadero
    My special thanks to PVS-Studio
    My special thanks to Tipi.build 
    My special thanks to Take Up Code
    My special thanks to SHAVEDYAKS

    Seminars

    I’m happy to give online seminars or face-to-face seminars worldwide. Please call me if you have any questions.

    Standard Seminars (English/German)

    Here is a compilation of my standard seminars. These seminars are only meant to give you a first orientation.

    • C++ – The Core Language
    • C++ – The Standard Library
    • C++ – Compact
    • C++11 and C++14
    • Concurrency with Modern C++
    • Design Pattern and Architectural Pattern with C++
    • Embedded Programming with Modern C++
    • Generic Programming (Templates) with C++
    • Clean Code with Modern C++
    • C++20

    Online Seminars (German)

    Contact Me

    Modernes C++ Mentoring,

     

     

    0 replies

    Leave a Reply

    Want to join the discussion?
    Feel free to contribute!

    Leave a Reply

    Your email address will not be published. Required fields are marked *