Experience Embedded

Professionelle Schulungen, Beratung und Projektunterstützung

C++ Core Guidelines Reloaded

Autor: Prof. Peter Sommerlad, HSR Rapperswil

Beitrag - Embedded Software Engineering Kongress 2017

 

The C++ Core Guidelines are an effort led by Bjarne Stroustrup. They intend to show how to use modern C++ effectively. "Following the rules will lead to code that is statically type-safe, has no resource leaks, and catches many more programming logic errors than is common in code today. And it will run fast -- you can afford to do things right." In addition to the freely adaptable core guidelines, there also exists an open source support library providing features for code transition and support that might not be yet in your C++ standard library, or even standardized.

However, there are many, many rules in the C++ Core Guidelines. Sometimes, I have the impression there are too many and some of them seem to stem from analyzing large code bases and taking every potential problem in there into account.

Also further aspects seem to be missing or well hidden, while I consider others, such as marking ownership of pointers. So where should we start and how? What are missing parts? Can we check for violations of guidelines and what can we do to remedy them? Do other organizations also care for the core guidelines? While they claim for code safety, is there any official blessing (like MISRA) that I can refer to when producing safety-related C++ code? Many, many questions...

Pointers

The C++ core guidelines recommend to minimize the use of plain pointers for referring to single objects only. In the past, especially due to C's lack of references, pointers seem to prevail. I met programmers who had the impression that object-oriented programming with dynamic polymorphism in C++ requires to use heap-allocated objects and pointers. That is plain wrong. You can pass (local) objects of a subclass to functions taking references to base classes to employ dynamic polymorphism. That is what happens even in the classic C++ hello world program, when you write

std::cout << "Hello, world! "s;

In my opinion, plain pointers should be banned completely. For contiguous sequences of objects, where pointer arithmetic is used today, you should use either string_view (introduced into std with C++17) or span<T> (in the guideline support library) and iterate using their provided iterators. For passing a single object to a function, where today you use a pointer, consider using a (const) reference instead.

Functions returning pointers should be adapted/wrapped to either return a unique_ptr<T>, if they transfer ownership, or should return an optional<T> by value, if the nullptr is used to denote an absence of a result, e.g., to denote an error condition. optional<T> can also be used for optional parameters, where today a nullptr denotes a missing argument. For large parameter types that you do not want to pass by value and that are optional, optional<reference_wrapper<T>> is possible instead of using a pointer.

Arrays (ES.27, SL.con.1, SL.con.3)

C-style plain arrays have the disadvantage to decay to pointers when passed as function arguments, unless they are passed by reference (I.13: Do not pass an array as a single pointer). Then the dimension can be deduced as a template argument.

template <size_t N>

auto foo(int (&a)[N]){...}

You should use either std::array<T,N> for fixed sized arrays or std::vector<T>, if the number of elements can vary. C++ does not provide arrays with dynamic dimension as C's variable-length arrays(VLA), because those won't fit the type system. Use vector instead, unless the elements have the type bool, a special case that is a case of premature optimization that created many problems down the road.

Rule of Zero

I always tell my students and in my conference talks to follow the "Rule of Zero", that means to rely on compiler-provided special member functions such as the destructor and copy/move operations. That means, not even declaring them as defaulted.

There are a few exceptions to that rule:

  • One is the situation, where an object-oriented class hierarchy is built, then it is wise to declare a defaulted virtual destructor in the base class and delete the copy operations (C.67: A base class should suppress copying). The first guarantees correct destruction through a base class unique_ptr for heap-allocated derived objects, the second avoids unintended object slicing. It is considered a bug due to legacy specifications that copy operations are still provided when a user declares a destructor. Corresponding move operations are not provided by the compiler in that case.
  • There exists a further reason for defining own special member functions: The implementation of a RAII class for resource management (see below). Such classes will have deleted copy operations and should be moveable, so that the clean-up will only occur once. That means, the moved-from state of such classes must be detectable in the destructor and sidestep the cleanup code then. For example, a "moved-from" unique_ptr<T> will contain nullptr where delete is a no-op.
  • A final category are container classes that are prepared to take arbitrary numbers of non-default-constructable type value, such as std::vector<T>. There you need to provide special work to call the destructors of the stored values. If you can live without the rare non-default constructible value types (C.43: Ensure that a value type class has a default constructor), you should opt for a std::unique_ptr<T[]> for dynamic storage, which will be able to store an allocated number of objects on the heap and destroy all of them when destroyed.

 

That means, except for these three cases, you can stick with the rule of zero and let the compiler do the right thing for you. And even in those three cases there are means to minimize the reason/effort to implement your own special member functions.

Resource Management (excerpt)

While many rules consider resource management, there are some missing ingredients I would like to share. RAII (resource acquisition is initialization) is the key to safe and non-leaking resource management. You should rely on standard library classes that do the resource management. Whenever you are tempted to write your own code that should manage a resource, implement the RAII class to do just that, resource management of a single resource. Why? Implementing exception safe resource management for multiple resources is hard to do correctly, up to impossible, if you do not control the type of resources to handle. I know, because I needed world-class expert help by Eric Niebler to specify and implement unique_resource<T,D> a generic resource management class for any sanely moveable/copyable type T and a "Deleter" functor D (http://wg21.link/p0052). I recommend that you get and use unique_resource, even if it didn't make it into C++17 (https://github.com/PeterSommerlad/SC22WG21_Papers/tree/master/workspace/P0052_scope_exit)

If you really want to implement your own RAII class, make sure it is move-only, non-copyable and comes with either a factory (if generic) or a constructor establishing the resource to be released in its destructor.

In case you need to combine multiple resources, use their RAII objects as member variables, instead of the plain handles, because it is quite tricky to guarantee strong exception safety, when you need to recover from partial initialization failure. When initialization of a member variable fails (e.g., because of "C.42: If a constructor cannot construct a valid object, throw an exception"), the already initialized member variables will be automatically destructed by the compiler-generated code. Be aware that member initialization always happens in the sequence of the declarations of a class member variables.

Const

For me, that is a done deal. Use Cevelop's "Constificator" plug-in to analyze and update your code base to make it const-clean. You can even rewrite your code, so that const will be on the right side, making reading the type inside out much easier. Not applying const, where it could be placed has been a mistake. Unfortunately, due to C++'s C legacy, const is not the default in most places, where it should be.

The core guidelines mention const in (P.1: Express ideas directly in code) and many other places.

Come to my talk to learn more and how MISRA and AUTOSAR are working on new modern and better C++ guidelines based on the C++ Core Guidelines of which many we already check in Cevelop and where we also help you to semi-automatically make your code conform to by quick-fixes and refactorings. I suggest that you use not only tools checking for guideline violations on a build server, but use tools that give immediate feedback and help you to write better code. If feedback comes to late or violation messages pile up without help for remedy, most often the messages get ignored (or suppressed) instead of leading to better code.

Reference

 

Beitrag als PDF downloaden


Implementierung - unsere Trainings & Coachings

Wollen Sie sich auf den aktuellen Stand der Technik bringen?

Dann informieren Sie sich hier zu Schulungen/ Seminaren/ Trainings/ Workshops und individuellen Coachings von MircoConsult zum Thema Implementierung /Embedded- und Echtzeit-Softwareentwicklung.

 

Training & Coaching zu den weiteren Themen unseren Portfolios finden Sie hier.


Implementierung - Fachwissen

Wertvolles Fachwissen zum Thema Implementierung/ Embedded- und Echtzeit-Softwareentwicklung steht hier für Sie zum kostenfreien Download bereit.

Zu den Fachinformationen

 
Fachwissen zu weiteren Themen unseren Portfolios finden Sie hier.