This article presents a list of good practices for C++ development. Obviously there are many other good practices that one should adhere to and perhaps some of them are more important than the ones in this list. The following list is a personal recommendation and should be taken as is.

Thou shalt follow the Rule of Five

Before the advent of C++11 this was known as the Rule of Three. The rule said that if a class needs to define one of the following members it has to define all of them: destructor, copy constructor and copy assignment operator. When C++11 was released it introduced move semantics and the old Rule of Three has been extended to include two new special functions: move constructor and move assignment operator.

All these are special functions. If you don’t implement them explicitly, the compiler provides a default implementation. Make sure that when you implement one of them you implement them all. (There are exceptions to this rule, but that is beyond the scope of this article.)

Thou shalt use almost always auto (judiciously)

Using auto for type deduction of variables or parameters is a key feature of C++11. Using auto for variables instructs the compiler to deduce the type in the same manner it deduces the type of parameters of function templates (with a small exception related to std::initializer_list ). There are two ways to declare variables using auto :

auto x = value; // no type specified auto x = type {value}; // type explicitly specified 1 2 auto x = value ; // no type specified auto x = type { value } ; // type explicitly specified

There are some gotchas though that you should be aware of:

auto does not retain constness/volatileness ( const and volatile ) or reference-ness ( & and && ). Here is an example: int const x = 42; int const &r = x; auto a = x; // type of a is int auto ar = r; // type of ar is int 1 2 3 4 5 int const x = 42 ; int const &r = x; auto a = x ; // type of a is int auto ar = r ; // type of ar is int If you expect that the type of a is int const and the type of ar is int const& then you’re wrong. They are both simply int . You need to explicitly add const and & to retain the const-ness and reference-ness. auto const a = x; // type of a is int const auto const &ar = r; // type of ar is int const& 1 2 auto const a = x ; // type of a is int const auto const &ar = r; // type of ar is int const&

does not retain constness/volatileness ( and ) or reference-ness ( and ). Here is an example: auto captures initializer_list as a type. Here is an example: int const x = 42; auto a = x; // type of a is int auto b = {x}; // type of b is initializer_list<int> auto c {x}; // type of c is initializer_list<int> 1 2 3 4 5 int const x = 42 ; auto a = x ; // type of a is int auto b = { x } ; // type of b is initializer_list<int> auto c { x } ; // type of c is initializer_list<int> The type of a is int , but the type of both b and c is initializer_list<int> .

captures as a type. Here is an example: the form where you commit to a type does not work with multi-word build in types, nor with elaborated type specifiers (e.g. “struct tag”): auto x = long long {42}; // error auto t = struct tag {42}; // error 1 2 auto x = long long { 42 } ; // error auto t = struct tag { 42 } ; // error

Though many consider auto a nice feature to save typing because you don’t have to write long type names that is probably the least important reason to use it. There are more important reasons such as correctness, robustness and maintainability. When you specify variable types explicitly you can leave the variables uninitialized. But when you use auto you must initialize the variable (so that the compiler can infer the type). Using auto helps thus avoiding uninitialized variables. It also helps programming towards interfaces not implementations. Most of the times you don’t care about the type, you only care about what a variable does. And when you still care about the type, you can still use auto .

C++14 introduces two new features that extends the way auto can be used: function return type deduction (that allows auto to be used for the return type) and generic lambdas (that allows lambda parameter to be declared with the auto type specifier). There are various scenarios and pros and cons for using auto as the return type of a function. Most of them are probably of personal preferences. I personally do not favor the use of auto as function return type mainly for readability and documentation (reference documentation where all functions return auto is not very helpful). Unlike variables, where the type is not important many times, I believe the return type of a function is important most of the times.

This is a large and complex subject and I recommend some additional readings: Auto Variables, Part 1, Auto Variables, Part 2, AAA Style (Almost Always Auto).

Thou shalt use use smart pointers

Use of raw pointers in C++ (that implies explicit allocation and release of memory) is one of the most hated features of the language (despite the advantages they pose) because it is one of the most importance source of bugs in C++ development. Developers tend to forget to release memory when no longer necessary. Smart pointer come to rescue. They look and behave like naked pointers, by supporting operations like dereferencing (operator *) and indirection (operator ->), but they do more than just that, hence the adjective “smart”. A smart pointer is a proxy to a raw pointer and basically handles the destruction of the object referred by the raw pointer. The standard library provides a std::shared_ptr class for objects whose ownership must be shared and a std::unique_ptr for objects that do not need shared ownership. The first one destroys the pointed object when the last shared pointer object that points to the object is destroyed, the second when the smart pointer is destroyed (since it retains sole ownership of the object). There is another smart pointer, std::weak_ptr that holds a non-owning reference to an object managed by a std::shared_ptr . These smart pointers provide a deterministic way of destructing objects in a safe manner, avoiding memory leaks that are so easily introduced with raw pointers. Smart pointers can be created in an exception safe manner by using the std::make_shared and std::make_unique functions from the standard library.

Thou shalt use smart classes/resources (RAII)

What I call “smart class” or “smart resource” is known as RAII (Resource Acquisition Is Initialization), CADRe (Constructor Acquires, Destructor Releases) or SBRM (Scope-based Resource Management). I don’t like any of those names because they are so cryptic. Inspired from the term smart pointers, I like to call RAII smart resources. RAII is a programming idiom for exception-safe resource management. Acquisition of resources is done in the constructor an the release in the destructor, thus avoiding resource leaks. This is a generalization of the smart pointers, where the resource is memory. In case of RAII it can be anything, a system handle, a stream, a database connection, etc.

Using smart pointers is not enough if you do not take the extra step and use smart resources too. Consider the following example where we write to a file:

HANDLE hFile = CreateFile(L"", GENERIC_WRITE, 0, NULL, CREATE_NEW, FILE_ATTRIBUTE_NORMAL, NULL); if(hFile != INVALID_HANDLE_VALUE) { // write to the file CloseHandle(hFile); } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 HANDLE hFile = CreateFile ( L "" , GENERIC_WRITE , 0 , NULL , CREATE_NEW , FILE_ATTRIBUTE_NORMAL , NULL ) ; if ( hFile ! = INVALID_HANDLE_VALUE ) { // write to the file CloseHandle ( hFile ) ; }

This code has several issues. It is possible to forget to close the file handle (especially with larger code). Even if you close the handle, the code is not exception safe and the handle will not be closed if an exception occurs between opening the file and closing it.

These problems can be avoided by using a smart handle resource. The bellow implementation is the bare minimum and a real implementation may be more elaborated.

class smart_handle { HANDLE handle; public: smart_handle(HANDLE const h): handle(h) {} operator HANDLE() const {return handle;} operator bool() const {return handle != INVALID_HANDLE_VALUE;} ~smart_handle() { if(handle != INVALID_HANDLE_VALUE) CloseHandle(handle); } }; 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 class smart_handle { HANDLE handle ; public : smart_handle ( HANDLE const h ) : handle ( h ) { } operator HANDLE ( ) const { return handle ; } operator bool ( ) const { return handle ! = INVALID_HANDLE_VALUE ; } ~ smart_handle ( ) { if ( handle ! = INVALID_HANDLE_VALUE ) CloseHandle ( handle ) ; } } ;

The previous code can now change to:

smart_handle shFile = CreateFile(L"", GENERIC_WRITE, 0, NULL, CREATE_NEW, FILE_ATTRIBUTE_NORMAL, NULL); if(shFile) { // write to the file } 1 2 3 4 5 6 7 8 9 10 11 12 smart_handle shFile = CreateFile ( L "" , GENERIC_WRITE , 0 , NULL , CREATE_NEW , FILE_ATTRIBUTE_NORMAL , NULL ) ; if ( shFile ) { // write to the file }

Not only that the client code became simpler, it is also safer. The file handle is closed in the smart handle destructor. That means you cannot forget to close it, but also, in case of an exception, it is guaranteed to be closed, because the destructor of the smart handle will be called during stack unwinding.

Smart pointers and smart resources (RAII) enable you to write exception safe, leak-free code, with deterministic release of resource.

Thou shalt use std::string

std::string (and it’s wide character counterpart std::wstring ) should be the default and the de facto type for strings in C++. Using char* like in C has many drawbacks: you must allocate memory dynamically and make sure you release it correctly, you must have arrays large enough to accommodate actual values (what if you declared an array of 50 chars and you read 60?), are prone to ill-formed declarations ( char* name = "marius"; is incorrect and triggers a runtime exception if you attempt to change the data) and are not exception safe. The string classes from the standard library avoid all this problems: they handle memory automatically, can be modified, can be resized, they work with the standard algorithms and if an exception occurs the internal buffer is automatically freed when then object is destructed during stack unwinding.

Thou shalt use standard containers

std::string is not a built in type, but a special container for characters. The standard library provides other general purpose containers including std::vector , std::list , std::array , std::map , std::set , std::queue . You should use them accordingly to your needs. std::vector should be the default container (if the size is fixed and known at compile time then you should consider using std::array in that case). These containers, used appropriately, provide great performance and can be used uniformly with the standard algorithms. In practice it is rarely that these containers do not suit all your needs and you have to rely on other special implementations for better performance.

Thou shalt use standard algorithms and utilities

The C++ standard library provides many general purpose algorithms that you can use in your code. Don’t reinvent the wheel. If you need to count, search, aggregate, transform, generate, sort or many other operations you’ll find something already available in the standard library. Most algorithm are available in the <algorithm> header, but some of them can be found in the <numerics> header. Also many utility functions are available in the standard, such as functions to convert between string and numeric types. See the <cstdlib> for such utilities.

Thou shalt use namespaces

Unfortunately, namespaces are a C++ feature that is not used as much as it should. Like in any other language that supports them, namespaces provide a way to logically group functionality into units, but also help you avoid name collisions (because you cannot have two symbols with the same name in the same namespace, but you can have in two different namespaces).

Though library implementators do use namespaces (for the reason mentioned above) I’ve seen little use in line of business applications. A reason may be that IDEs like Visual Studio do not promote namespaces. No project and item templates for C++ in Visual Studio use namespaces. No code generated by a C++ wizard will be inside a namespace. In fact if you put MFC code into namespaces the Visual Studio wizards will no longer worked with your code.

Do use namespaces. It helps grouping your code logically and it helps avoiding name collisions.

Thou shalt use const

The const keyword can be used on variables and function parameters to indicate they are immutable, but also on non-static member functions to indicate that a function cannot alter member variables of a class, nor it can call any non-const member of the class.

The const keyword should be used on all variables that do not change their value and all member functions that do not alter the state of the object. This helps not only better documenting your code, but also allow the compiler to immediately flag incorrect use of immutable variables or functions and also give it a chance to better optimize your code.

Let’s consider the following (dummy) example of a function:

int foo(int a) { int x = get_value(); if(x > 0) return a+x; return a * a; } 1 2 3 4 5 6 7 8 int foo ( int a ) { int x = get_value ( ) ; if ( x > 0 ) return a + x ; return a * a ; }

Neither the parameter a nor the variable x change their value, so they should be both declared as const .

int foo(int const a) { int const x = get_value(); if(x > 0) return a+x; return a * a; } 1 2 3 4 5 6 7 8 int foo ( int const a ) { int const x = get_value ( ) ; if ( x > 0 ) return a + x ; return a * a ; }

It is very easy to omit the const keyword and in practice I have seen little use of it. I strongly recommend taking the effort to put const wherever possible to ensure const correctness of your programs.

Thou shall use virtual and override (and final)

This may seem of little importance comparing to other practices in this list, but I personally find in important especially for code readability and maintainability. Unfortunately, C++ does not enforce you to specify the virtual keyword on derived classes in a hierarchy to indicate that a function is overriding a base class implementation. Having virtual in the class where the function is first declared is enough. Many developers tend to ignore the virtual keyword on derived classes and that makes it hard to figure, especially on large code bases or large hierarchies which function is virtual and is actually overriding a base implementation.

This is a bad example class foo { protected: virtual void f(); }; class bar : public foo { protected: void f(); }; 1 2 3 4 5 6 7 8 9 10 11 class foo { protected : virtual void f ( ) ; } ; class bar : public foo { protected : void f ( ) ; } ;

C++11 has added two new reserved words, override and final to actually indicate that a virtual function is overriding another implementation, or that a virtual function can no longer be overridden. These should be used on all virtual methods accordingly.

This is the good practice class foo { protected: virtual void f(); }; class bar : public foo { protected: virtual void f() override; }; 1 2 3 4 5 6 7 8 9 10 11 class foo { protected : virtual void f ( ) ; } ; class bar : public foo { protected : virtual void f ( ) override ; } ;

Share this: Facebook

Twitter

Print

More

Email

Reddit



