industrsy

C++ Core Guidelines: Bounds Safety

Today’s post concerns the second C++ Core Guidelines: Bounds Safety profile. The goal of the profile bounds safety is that you operate inside the bounds of allocated memory.

 

industrsy

The profile names the two enemies for bounds safety: pointer arithmetic and array indexing. Additionally, when you use a pointer, it should only address a single object but not an array. To complete the profile bounds safety, you should combine it with the rules to type safety and lifetime safety. Type safety was the topic of my two previous posts: C++ Core Guidelines: Type Safety and C++ Core Guidelines: Type Safety by Design. Lifetime safety will be the topic of my next post.

Bounds Safety

Bounds safety consists of four rules:

  • Bounds.1: Don’t use pointer arithmetic
  • Bounds.2: The only index into arrays using constant expressions
  • Bounds.3: No array-to-pointer decay
  • Bounds.4: Don’t use standard-library functions and types that are not bounds-checked

The four rules to bounds safety mention three rules of the C++ core guidelines. As in the last posts to the profiles, I will make my additions if necessary.

Bounds.1: Don’t use pointer arithmetic, Bounds.2: Only index into arrays using constant expressions and Bounds.3: No array-to-pointer decay

The reason for the three rules boils down to the three do’s: pass pointers to single objects (only), keep pointer arithmetic simple, and use std::span. The first do can also be formulated negatively: don’t pass pointers to arrays. I assume you don’t know std::span. std::span<T> represents a non-owning range of contiguous memory. This range can be an array, a pointer with a size, or a std::vector.

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:

 

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)
  • "Embedded Programming with Modern C++": January 2025
  • "Generic Programming (Templates) with C++": February 2025
  • "Clean Code: Best Practices for Modern C++": May 2025
  • Do you want to stay informed: Subscribe.

     

    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 get 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 counted the number of elements wrong, or the size of the C-array changed. Anyway, the result is the same: undefined behavior. The same argumentation will also hold for a C-string.

    What should we do? We should use a suitable data type. C++20 supports std::span. 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! std::span checks at run-time its boundaries.

    But I hear your complaints: We don’t have C++20. No problem. It’s pretty easy to rewrite the functions f 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); 
    }
    
    int main(){
    
        std::array<int, 100> arr{};
    
        f(arr);
        
    }
    

     

    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 std::span version, and second, the size of the std::array is part of the signature of the function f. This isn’t good. 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, overcoming the type restrictions is easy but staying 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;                 // (5)
        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);                                          // (6)
    }
    
    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);                                                 
        
    }
    

     

    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). These containers have in common that their data is stored in a contiguous memory block. This will no hold std::deque; therefore, the call a.data() in expression (6) 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.

    Bounds.4: Don’t use standard-library functions and types that are not bounds-checked

    I have already written a post C++ Core Guidelines: Avoid Bounds Errors. This post gives background information to this rule and provides do’s. 

    What’s next?

    The name of the third profile is Lifetime Safety Profile. This profile which is the topic of my next post, boils down to one rule: Don’t dereference a possibly invalid pointer.

     

     

    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 *