C++ Core Guidelines: Rules for Statements

Contents[Show]

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 behaviour. 

 

UnderConstruction

Here are the two remaining rules for expressions.

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

The reason for using T{e} for the construction of a value is quite obvious. 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 times, not your intention. Have a look at the example from the guidelines.

 

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.

There is a special rule that you have to keep in mind if you construct a value with T(e1, e2) or T{e1, e2}. What will happen if you have a class 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.

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

  

constructionWithBracesError

A constructor taking an 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, having the value 1; the second line will create a vector, having 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 behaviour. 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 to the different kinds of ownership semantic in modern C++. Read the details here: C++ Core Guidelines: Rules to Resource Management.

Let's switch gears. 

Rule for statements

The rules for statements are quite obvious; therefore, I can make it short.

  • You should 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 optimised.
  • The same holds for a range-based for loop (ES.71) in contrast 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 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 not only hold for a for-loop but also 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), goto-statements (ES.76), and minimise 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. Afterwards, the arithmetic rules become more thrilling.

 

 

Thanks a lot to my Patreon Supporters: Eric Pederson, Paul Baxter, Carlos Gomes Martinho, and SAI RAGHAVENDRA PRASAD POOSA.

 

 

Get your e-book at leanpub:

The C++ Standard Library

 

Concurrency With Modern C++

 

Get Both as one Bundle

cover   ConcurrencyCoverFrame   bundle
With C++11, C++14, and C++17 we got a lot of new C++ libraries. In addition, the existing ones are greatly improved. The key idea of my book is to give you the necessary information to the current C++ libraries in about 200 pages.  

C++11 is the first C++ standard that deals with concurrency. The story goes on with C++17 and will continue with C++20.

I'll give you a detailed insight in the current and the upcoming concurrency in C++. This insight includes the theory and a lot of practice with more the 100 source files.

 

Get my books "The C++ Standard Library" (including C++17) and "Concurrency with Modern C++" in a bundle.

In sum, you get more than 550 pages full of modern C++ and more than 100 source files presenting concurrency in practice.

 

Add comment


My Newest E-Books

Latest comments

Subscribe to the newsletter (+ pdf bundle)

Blog archive

Source Code

Visitors

Today 627

All 844781

Currently are 223 guests and no members online

Kubik-Rubik Joomla! Extensions