C++ Core Guidelines: Rules for Expressions and Statements

There are quite a lot of rules in the C++ Core Guidelines dealing with expressions and statements. To be precise, there are more than 50 rules are about declarations, expressions, statements, and arithmetic expressions.

naming standards clean code 24 728 

 

I forget to mention two rules that are just called general. Here we are.

ES.1: Prefer the standard library to other libraries and to “handcrafted code”

There is no reason to write a raw loop, to sum up a vector of doubles:

int max = v.size();             // bad: verbose, purpose unstated
double sum = 0.0;
for (int i = 0; i < max; ++i)
    sum = sum + v[i];

 

You should just use the std::accumulate algorithm from the STL.

auto sum = std::accumulate(begin(a), end(a), 0.0);   // good

 

This rule reminds me to a sentence from Sean Parent at CppCon 2013: "If you want to improve the code quality in your organization, replace all your coding guidelines with one goal: No raw loops!"

Or to say it more directly: If you write a raw loop, you probably don't know the algorithms of the STL.

ES.2: Prefer suitable abstractions to direct use of language features

The next déjà vu. In one of my last C++ seminars, I had a long discussion followed by an even longer analysis of a few quite sophisticated and handmade functions for reading and writing strstreams. The participants had to maintain this functions and had after one week no idea what was going on.

The main obstacle to don't understand the functionality was that the functionality was not based on the right abstraction.

For example, compare the handmade function for reading an std::istream.

char** read1(istream& is, int maxelem, int maxstring, int* nread)   // bad: verbose and incomplete
{
    auto res = new char*[maxelem];
    int elemcount = 0;
    while (is && elemcount < maxelem) {
        auto s = new char[maxstring];
        is.read(s, maxstring);
        res[elemcount++] = s;
    }
    nread = &elemcount;
    return res;
}

 

In contrast, how easy is the following function to consume?

vector<string> read2(istream& is)   // good
{
    vector<string> res;
    for (string s; is >> s;)
        res.push_back(s);
    return res;
}

 

The right abstraction often means you have not to think about ownership such in the function read1. This will not hold for the function read2. The caller of read1 is the owner of result and has to delete it.

A declaration introduces a name into a scope. To be honest, I'm biased. On one hand, the following rules are little bit borrowing for you, because they are quite obvious. On the other hand, I know a lot of codebases which permanently break this rules. For example, I had a discussion with a former Fortran programmer, who stated: Each variable should have exactly three characters.

Anyway, I will continue presenting the rules, because good names a probably the key to make code readable, understandable, maintainable, extensible, ... .

Here are the first six rules.

ES.5: Keep scopes small

If a scope is small, you can put it on a screen and get an idea what is going on. If a scope becomes too big, you should structure your code into function or objects with methods. Identify logical entities and use self-explanatory names in your refactoring process. Afterward, it is a lot easier to think about your code.

ES.6: Declare names in for-statement initializers and conditions to limit scope

Since the first C++ standard, we can declare a variable in a for statement. Since C++17, we can declare variables in an if or a switch statement.

std::map<int,std::string> myMap;

if (auto result = myMap.insert(value); result.second){  // (1)
    useResult(result.first);  
    // ...
} 
else{
    // ...
} // result is automatically destroyed                 // (2)

 

The variable result (1) is only valid inside the if and else branch of the if statement. result will not pollute the outer scope and will be automatically destroyed (2). This can not be done before C++17. You have to declare result in the outer scope (3).

 

std::map<int,std::string> myMap;
auto result = myMap.insert(value)   // (3)
if (result.second){  
    useResult(result.first);  
    // ...
} 
else{
    // ...
} 

 

ES.7: Keep common and local names short, and keep uncommon and nonlocal names longer

This rule sounds strange but we are already used to it. Giving a variable the name i or j, or giving a variable the name T will make the intention of the code immediately clear: i and j are indices, and T is a type parameter of a template.

template<typename T>    // good
void print(ostream& os, const vector<T>& v)
{
    for (int i = 0; i < v.size(); ++i)
        os << v[i] << '\n';
}

 

There is a meta-rule behind this rule. A name should be self-explanatory. In a short context, you get with a glance what the variable means. This will not automatically hold for longer contexts; therefore, you should use longer names. 

ES.8: Avoid similar-looking names

Can you read this example without any hesitation?

if (readable(i1 + l1 + ol + o1 + o0 + ol + o1 + I0 + l0)) surprise();

 

To be honest, I often have problems with the number 0 and the big capital O. Depending on the used font it looks quite similar. Two years ago it took me quite a time to log into a server. My automatically generated password had a character O.

ES.9: Avoid ALL_CAPS names

If you use ALL_CAPS macro substitution may kick in because ALL_CAPS are commonly used for macros. The following program snippet may have a little surprise involved.

// somewhere in some header:
#define NE !=

// somewhere else in some other header:
enum Coord { N, NE, NW, S, SE, SW, E, W };

// somewhere third in some poor programmer's .cpp:
switch (direction) {
case N:
    // ...
case NE:
    // ...
// ...
}

 

ES.10: Declare one name (only) per declaration

Let me give you two examples. Did you spot the two issues?

 

char* p, p2;
char a = 'a';
p = &a;
p2 = a;                              // (1)

int a = 7, b = 9, c, d = 10, e = 3;  // (2)

 

p2 is just a char  (1) and c is not initialised (2).

With C++17, we got one exception to this rule: structured binding.

Now, I can write the if statement with initialiser in rule ES.6 even cleaner and more readable.

 

std::map<int,std::string> myMap;

if (auto [iter, succeeded] = myMap.insert(value); succedded){  // (1)
    useResult(iter);  
    // ...
} 
else{
    // ...
} // iter and succeeded are automatically destroyed            // (2)

 

What's next?

Of course, I will continue in my next post with the rules regarding declarations.

 

 

 

Thanks a lot to my Patreon Supporters: Eric Pederson, Paul Baxter, Franco Amato, and Carlos Gomes Martinho.

 

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.

 

Comments   

0 #1 Christena 2018-02-12 19:17
Good way of describing, and fastidious paragraph to take data concerning my presentaion topic,
which i am goiong too present in institution of higher education.
Quote

Add comment


Subscribe to the newsletter (+ pdf bundle)

Blog archive

Source Code

Visitors

Today 2626

All 1164606

Currently are 163 guests and no members online

Kubik-Rubik Joomla! Extensions

Latest comments