UnderConstruction

C++ Core Guidelines: Rules for Statements

Before I continue with the roughly 15 rules for statements, let me finish the two rules for expressions. Both rules help you to protect your program from undefined behavior. 

 

UnderConstruction

Here are the two remaining rules for expressions.

ES.64: Use the T{e}notation for construction

The reason for using T{e} to construct a value is quite apparent. In contrast to T(e) or (T)e, T{e} does not allow narrowing conversion. Narrowing conversion is a conversion including the loss of data accuracy. I assume this is, most of the time, not your intention. Have a look at the example 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.

     

    void use(char ch, double d, char* p, long long lng){
        int x1 = int{ch};     // OK, but redundant
        int x2 = int{d};      // error: double->int narrowing; use a cast if you need to
        int x3 = int{p};      // error: pointer to->int; use a reinterpret_cast if you really need to
        int x4 = int{lng};    // error: long long->int narrowing; use a cast if you need to          (1)
    
        int y1 = int(ch);     // OK, but redundant
        int y2 = int(d);      // bad: double->int narrowing; use a cast if you need to
        int y3 = int(p);      // bad: pointer to->int; use a reinterpret_cast if you really need to  (2)
        int y4 = int(lng);    // bad: long->int narrowing; use a cast if you need to
    
        int z1 = (int)ch;     // OK, but redundant
        int z2 = (int)d;      // bad: double->int narrowing; use a cast if you need to
        int z3 = (int)p;      // bad: pointer to->int; use a reinterpret_cast if you really need to  (3)
        int z4 = (int)lng;    // bad: long long->int narrowing; use a cast if you need to            
    }
    

     

     Here is what Gcc provides without any special flags.

     Screenshot 20180223 192512

    If you carefully read the output of the compiler run, you will observe a few interesting facts.

    • Expression (1) will only give a warning in the first code block; the two previous expressions will produce an error.
    • Only the expressions (2) and (3) result in an error. The other conversions in the second and third code block will not even give a warning.

    You must keep a particular rule in mind if you construct a value with T(e1, e2) or T{e1, e2}. What will happen if you have a class that has two competing constructors? One constructor accepting two ints (MyVector(int, int)) and the other accepting an std::initializer_list<int> (MyVector(std::initializer_list<int>))? The interesting question is: Does a call MyVector(1, 2) or a call MyVector{int, int} the constructor for two ints or the one with the std::initalizer_list<int>?

     

    // constructionWithBraces.cpp
    
    #include <iostream>
    
    class MyVector{
    public:
        MyVector(int, int){
            std::cout << "MyVector(int, int)" << std::endl;
        }
        MyVector(std::initializer_list<int>){
            std::cout << "MyVector(std::initalizer_list<int>)" << std::endl;
        }
    };
    
    class MyVector1{
    public:
        MyVector1(int, int){
            std::cout << "MyVector1(int, int)" << std::endl;
        }
    };
    
    class MyVector2{
    public:
        MyVector2(int, int){
            std::cout << "MyVector2(int, int)" << std::endl;
        }
    };
    
    int main(){
        
        std::cout << std::endl;
        
        MyVector(1, 2);                       // (1)
        MyVector{1, 2};                       // (2) 
        
        std::cout << std::endl;
        
        MyVector1{1, 2};                      // (3)
        
        std::cout << std::endl;
        
        MyVector2(1, 2);                      // (4)
        
        std::cout << std::endl;
        
    }
    

     

     

    Here is the output of the program. The call (1) calls the constructor with two ints; the call (2) the constructor with the std::initializer_list<int>. If you invoke MyVector1{1, 2} (3), der constructor MyVector1(1, 2) is a kind of fallback.

    They will not hold for (4). In this case, the constructor with the std::initializer_list<int> is not the fallback. 

      

    constructionWithBracesError

    A constructor taking a std::initializer_list as an argument is often called a sequence constructor. 

    Do you know why I called the class in the example MyVector? The reason is that the two following expressions behave differently.

     

    std::vector<int> vec(10, 1);  // ten elements with 1
    std::vector<int> vec2{10, 1}; // two elements 10 and 1
    

     

    The first line creates a vector of 10 elements with the value 1; the second line will create a vector with the values 10 and 1.

    ES.65: Don’t dereference an invalid pointer

    Let me put it this way. If you dereference an invalid pointer, such as a nullptr, your program has undefined behavior. This is nasty. The only way to avoid this is to check your pointer before its usage.

    void func(int* p) {
        if (p == nullptr) { // do something special
        }
        int x = *p;
        ...
    }
    

     

    How can you overcome this issue? Don’t use a naked pointer. Use a smart pointer such as std::unique_ptr or std::shared_ptr or a reference.  I have already written a post on the different kinds of ownership semantics in modern C++. Read the details here: C++ Core Guidelines: Rules to Resource Management.

    Let’s switch gears. 

    Rule for statements

    The statement rules are pretty obvious; therefore, I can make it short.

    • It will help if you prefer a switch statement to an if statement when there is a choice (ES.70) because a switch statement may be more readable and can be better optimized.
    • The same holds for a range-based for-loop (ES.71) compared to a for-loop. First, a range-based for loop is easier to read, and second, you can not make an index error or change the index while looping.
    • When you have an obvious loop variable, you should use a for-loop instead of a while statement (ES.72); if not, you should use a while statement (ES.73).

    (1) shows an example of when you should prefer a for loop and (2) when you should prefer a while statement.

     

    for (gsl::index i = 0; i < vec.size(); i++) {  // (1)
        // do work
    }
    
    int events = 0;                                // (2)
    while (wait_for_event()) {   
        ++events;
        // ...
    }

     

    • You should declare a loop variable in a for-loop (ES.74). This will hold for a for-loop and since C++17 for an if– or switch-statement. Read the details here: C++17 – What’s new in the core language?
    • Avoid do-statements (ES.75) and goto-statements (ES.76), and minimize the use of break and continue in loops (ES.77) because they are difficult to read. If something is difficult to read, it’s also error-prone.

    What’s next?

    There are a few rules for statements left. My next post will start with them. Afterward, the arithmetic rules become more thrilling.

     

     

     

    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, Matthieu Bolt, 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, and Philipp Lenk.

    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 *