dental

C++ Core Guidelines: More Rules for Expressions

I know this post’s headline is a bit boring: More Rules for Expressions. Honestly, this post is about code hygiene because I will mainly write about pointers.

 

dental

Let’s have a look at my plan for today.

I will start with a significant rule.

ES.42: Keep use of pointers simple and straightforward

Let me cite the words of the guidelines: “Complicated pointer manipulation is a major source of errors.”. Why should we care? Of course, our legacy code is full of functionality, such as this example:

void f(int* p, int count)
{
    if (count < 2) return;

    int* q = p + 1;    // BAD

    int n = *p++;      // BAD

    if (count < 6) return;

    p[4] = 1;          // BAD

    p[count - 1] = 2;  // BAD

    use(&p[0], 3);     // BAD
}

int myArray[100];     // (1)

f(myArray, 100),      // (2)

 

The main issue with this code is that the caller must provide the correct length of the C-array. If not, we have undefined behavior.

Think about the last lines (1) and (2) for a few seconds. We start with an array and remove its type information by passing it to the function f. This process is called an array-to-pointer decay and is the reason for many errors. Maybe we had a bad day, and we count the number of elements wrong, or the size of the C-array changed. Anyway, the result is always the same: undefined behavior. The same argumentation will also hold for a C-string.

 

Rainer D 6 P2 500x500Modernes C++ Mentoring

  • "Fundamentals for C++ Professionals" (open)
  • "Design Patterns and Architectural Patterns with C++" (open)
  • "C++20: Get the Details" (open)
  • "Concurrency with Modern C++" (open)
  • "Generic Programming (Templates) with C++": October 2024
  • "Embedded Programming with Modern C++": October 2024
  • "Clean Code: Best Practices for Modern C++": March 2025
  • Do you want to stay informed: Subscribe.

     

    What should we do? We should use the right data type. The Guidelines suggest using gsl::spantype from the Guidelines Support Library (GSL). Have a look here:

    void f(span<int> a) // BETTER: use span in the function declaration
    {
        if (a.length() < 2) return;
    
        int n = a[0];      // OK
    
        span<int> q = a.subspan(1); // OK
    
        if (a.length() < 6) return;
    
        a[4] = 1;          // OK
    
        a[count - 1] = 2;  // OK
    
        use(a.data(), 3);  // OK
    }
    

     

    Fine! gsl::span checks at run-time its boundaries. Additionally, the Guidelines Support Library has a free function at for accessing the elements of an gsl::span. 

    void f3(array<int, 10> a, int pos) 
    {
        at(a, pos / 2) = 1;       // OK
        at(a, pos - 1) = 2;       // OK
    }
    

     

    I know your issue. Most of you don’t use the Guidelines Support Library. No problem. It’s quite easy to rewrite the functions f and f3 using the container std::array and the method std::array::at. Here we are:

    // spanVersusArray.cpp
    
    #include <algorithm>
    #include <array>
    
    void use(int*, int){}
    
    void f(std::array<int, 100>& a){
    
        if (a.size() < 2) return;
    
        int n = a.at(0);      
    
        std::array<int, 99> q;
        std::copy(a.begin() + 1, a.end(), q.begin());      // (1)
    
        if (a.size() < 6) return;
    
        a.at(4) = 1;          
    
        a.at(a.size() - 1) = 2;
    
        use(a.data(), 3); 
    }
    
    void f3(std::array<int, 10> a, int pos){
        a.at(pos / 2) = 1;      
        a.at(pos - 1) = 2; 
    }
    
    int main(){
    
        std::array<int, 100> arr{};
    
        f(arr);
        
        std::array<int, 10> arr2{};
     
        f3(arr2, 6);
    
    }
    

     

    The std::array::at Operator will check at runtime its bounds. If pos >= size(), you will get an std::out_of_range exception. Looking carefully at the spanVersusArray.cpp program, you will notice two issues. First, the expression (1) is more verbose than the gsl::span version and second, the size of the std::array is part of the signature of the function f. This is bad. I can only use f with the type std::array<int, 100>.  In this case, the checks of the array size inside the function are superfluous. 

    To your rescue, C++ has templates; therefore, it’s easy to overcome the type restrictions but stay type-safe.

     

    // at.cpp
    
    #include <algorithm>
    #include <array>
    #include <deque>
    #include <string>
    #include <vector>
    
    template <typename T>
    void use(T*, int){}
    
    template <typename T>
    void f(T& a){
    
        if (a.size() < 2) return;
    
        int n = a.at(0);      
    
        std::array<typename T::value_type , 99> q;                 // (4)
        std::copy(a.begin() + 1, a.end(), q.begin());     
    
        if (a.size() < 6) return;
    
        a.at(4) = 1;          
    
        a.at(a.size() - 1) = 2;
    
        use(a.data(), 3);                                          // (5)
    }
    
    int main(){
    
        std::array<int, 100> arr{};                                             
        f(arr);                                                    // (1)
        
        std::array<double, 20> arr2{};
        f(arr2);                                                   // (2)
        
        std::vector<double> vec{1, 2, 3, 4, 5, 6, 7, 8, 9};
        f(vec);                                                    // (3)
        
        std::string myString= "123456789";
        f(myString);                                               // (4)
        
        // std::deque<int> deq{1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
        // f(deq);                                                 // (5)
        
    }
    

     

    Now, the function f works for std::array’s of different sizes and types (lines (1) and (2)) but also for a std::vector(3) or a std::string (4). This container has in common that its data is stored in a contiguous memory block. This will not hold std::deque; therefore, the call a.data() in expression (5) fails. A std::deque is a kind of doubly-linked list of small memory blocks.

     deque

    The expression T::value_type (5) helps me get each container’s underlying value type. T is a so-called dependent type because T is a type parameter of the function template f. This is the reason I have to give the compiler a hint that T::value_type is a type: typename T::value_type.

    ES.45: Avoid “magic constants”; use symbolic constants

    This is obvious: A symbolic constant says more than a magic constant. 

    The guidelines start with a magic constant, continue with a symbolic constant, and finish with a range-based for loop. 

    for (int m = 1; m <= 12; ++m)        // don't: magic constant 12
        cout << month[m] << '\n';
    
    
    
                      // months are indexed 1..12 (symbolic constant)
    constexpr int first_month = 1;
    constexpr int last_month = 12;
    
    for (int m = first_month; m <= last_month; ++m)        // better
        cout << month[m] << '\n';
    
    
    
    for (auto m : month)          // the best (ranged-based for loop)
        cout << m << '\n';
    

     

    In the case of the ranged-based for loop, it is not possible to make an off-by-one error.  

    Let me directly jump to the rule ES.47. I want to put the rules for conversion, including ES.46, in a separate post.

    ES.47: Use nullptr rather than 0 or NULL

    There are many reasons to use a nullptr instead of the number 0 or the macro NULL. In particular, 0 or NULL will not work in generic. I have already written a post about these three kinds of null pointers. Here are the details: The Null Pointer Constant nullptr.

    What’s next?

    How many explicit casts do we have in modern C++? Maybe your number is four, but this is the wrong number. In C++11, we have six explicit casts. When I Include the GSL, we have eight explicit casts. I will write about the eight casts in the next post.

     

    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, 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, 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, Philipp Lenk, Charles-Jianye Chen, Keith Jeffery, Matt Godbolt, and Honey Sukesan.

    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

    Modernes C++ GmbH

    Modernes C++ Mentoring (English)

    Do you want to stay informed about my mentoring programs? Subscribe Here

    Rainer Grimm
    Yalovastraße 20
    72108 Rottenburg

    Mobil: +49 176 5506 5086
    Mail: schulung@ModernesCpp.de
    Mentoring: www.ModernesCpp.org

    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 *