Data structures

Since arrays are blocks of static memory their size must be determined at compile time. Therefore the number of elements specified in the declaration must be a constant expression.

Initializing an array with no values will initialize each element to zero (e.g. int foo[5] = {};). The compiler can determine the number of elements from the initialization (e.g. int foo[] = {1, 2, 3, 4, 5};).

Arrays are passed by address even when not done explicitly. For example, calling void foo(int Bar[]); with foo(array); would not copy the entire array in memory. However the declaration of functions with a multi-dimensional array as a parameter must specify the size of all but the first dimension (e.g. void foo(int Bar[][3][4]);).

A vector represents a dynamically sized array that stores elements contiguously so that elements can be accessed using offsets.

Types

In addition to C typdef aliasing C++ provides the using keyword, which is more generic and has certain benefits when using templates.

typedef uint32_t FOO;
using FOO = uint32_t;

In addition to the enumerated type inherited from C, C++ provides the enum class that provide type safety.

enum class Weekdays {
    monday,
    tuesday,
    wednesday,
    thursday,
    friday
};

Weekdays work;
work = Weekdays::monday;

It is also possible to specify the underlying type:

enum class Weekend : char {saturday, sunday};

Conversion

Although casting can be perfectly safe, like integer promotion or up/downcasting, there are also pitfalls. Those include casting the following:

Casting

Explicit type casting, including c-like (e.g. Foo foo = (Bar) bar) or functional (e.g. Foo foo = int (bar)), is dangerous. C++ provides four casting operators:

Note that static_cast and reinterpret_cast only take pointers.

Check type

typeid returns a type_info that implements the == and != operators. It also provides a name() member that returns a null-terminated character sequence.

Foo *foo = new Bar;
typeid(foo).name;  // Foo *
typeid(*foo).name;  // Bar

Namespaces

Namespaces provide a more limited scope for variables, functions and compound types that would normally be globally scoped. They can be accessed from outside a namespace using the :: scope operator (e.g. namspaceName::x).

The using namespace <name> declaration allows unqualified access to elements of a library.

Initialization

In addition to classic C initialization (e.g. int x = 0;), C++ introduces constructor initialization, which encloses the initial value between parentheses (e.g. int x (0);). Additionally, C++ 11 introduced uniform initialization using curly braces (e.g. int x {0};).

Specifying auto instead of a type in a declaration tells the compiler to figure out the type of the variable. Similarly, the decltype specifier will use the type of a declared variable to declare a new variable. Note that using decltype may reduce the readability of the code.

Variables, functions and compound types may be globally or locally scoped. The later are declared inside code blocks.

Static storage like global or namespaced variables are initialized to zeroes when not explicitly initialized, unlike local variables that have an undetermined value when not explicitly initialized.

Strings

In addition to fundamental types, C++ provides numerous compound types like strings.

Arrays of characters can be declared as string literals using double quotes that are interpreted as literal constants. A null terminator is added to the end of the string.

Null-terminated character sequences can be transformed into strings implicitly, while strings can be transformed using c_str() or data members of string.

Streams

C++ provides an abstraction of input and output devices called streams. The standard library provides several stream objects: cin, cout, cerr, clog. The simplest way to write to and read from a stream is to use the insertion operator (i.e. <<) and extraction operator (i.e. >>). However, it is preferable to use getline(cin, x) to get input from a console.

A string can be converted to an integer using the stringstream in the <sstream> standard header:

string x ("1024");
int y;
stringstream(x) >> y;

Range-based for loop

Any range-based structure, which consists of a sequence of elements and implements the being and end functions may be used with a range-based for loop.

string example {"Hello world!"};
for (auto c : example) {
    std::cout << c;
}
std::cout << '\n';

Functions

Read-only passed by reference

To guarantee that parameters to a function that are passed by reference will not be modified, qualify the parameters as constant:

string concatenate(const string& first, const string& second)
{
    return first + second;
}

Inline specifier

Adding the inline specifier to a function declaration informs the compiler to insert the code within the function body into each place where the function is called. The compiler is likely already performing this optimization when necessary and is free to ignore the specifier.

Optional parameters

Assigning a value to a parameter in a function declaration (e.g. int val=32) indicates that the parameter is optional and sets that value as a default. For example, a function declared with two parameters, one of which has a default value, can be called with either one or two arguments.

Function templates

Function templates allow the definition of functions with generic types. The function template needs to be prefixed with the template keyword followed by template parameters that include class or typename that is in turn followed by an identifier, which can be used as a type within the function. When calling the function the user needs to specify the template arguments as well as the function arguments.

It is important to note that no code is generated from a template definition in a source file. In order for the compiler to generate an actual function the template arguments must be determined. When the compiler finds the first call for a given type it instantiates the function template and then calls that instantiation every time the function is used for the type. Function templates can be explicitly instantiated by declaring the template with a particular type as an argument.

#include <iostream>

using namespace std;

template <class Foo, class Bar>
Foo sum(Foo first, Bar second)
{
    Foo result;
    result = first + second;
    return result;
}

int main() {
    int i = 5;
    double j = 6, k;
    k = sum<int, double>(i, j); 
    cout << "Explicitly passing the types of the arguments: " << k << std::endl;
    k = sum(i, j); 
    cout << "Allowing the compiler to deduce the types: " <<  k << std::endl;
    return 0;
}

Pointers

The void pointer points to a value without defining a type and therefore cannot be dereferenced.

To point a pointer to nowhere without pointing to an invalid address, which would cause undefined behavior, assign nullptr to it (e.g. int *p = nullptr;).

Smart pointers

Raw pointers don’t indicate length, ownership, destruction, or whether it is dangling (i.e. it points to memory that no longer holds the original value). There are four types of smart pointers in C++11: std::auto_ptr, std::unique_ptr, std::shared_ptr, and std_weak_ptr. auto_ptr is deprecated in favor of unique_ptrs and should only be used to compile code with C++98 compilers.

Unique pointers models unique ownership, meaning that there is and will always be only one pointer pointing to an object. This is enforced by making them non-copyable. It is better to construct a smart pointer and assign an object to it in one statement to prevent the object from being assigned to multiple pointers.

Dynamic memory

To dynamically allocate memory C++ provides the new and delete operators. Unlike statically allocated memory (e.g. a regular array that must be a constant expression) the size can be determined at runtime.

Dynamic allocation is not guaranteed to be fulfilled by the system. It can be checked by catching a bad_alloc exception. Alternatively, the return value can be compared to a nullptr when the declaration specifies a nothrow object. Catching exceptions is preferred for efficiency.

int *foo;
foo = new int [5];
foo = new (nothrow) int [5];    // nullptr is returned instead of throwing `bad_alloc`
delete[5] foo;

However, new and delete should be used sparingly in favor of following RAII by using smart pointers.

Classes

class Foo
{
        int bar;
    public:
        void set_bar(int);
        int increment_bar(int);
} foo;  // Object of type Foo

// Use the scope operator to define a member function outside of the class
void Foo::set_bar(int x)
{
    bar = x;
}

Class members that are either data or functions may have access specifiers, which includes the keywords private, public, or protected. Members default to private access, unless declared after another specifier.

Defining a member function in a class definition causes it to be considered an inline function by the compiler, while defining it outside causes it to be considered a normal non-inline member function. The behavior is the same, but it may impact compiler optimizations.

Accessing members before they have been initialized returns an undefined value. Object initialization and storage allocation occurs within a constructor that has no return values.

class Foo
{
    int bar;
    Foo(int);
};

Foo::Foo()
{
    foo = 32;
}

Foo::Foo(int x)
{
    foo = x;
}

int main()
{
    Foo one(15);    // Functional form
    Foo two = 20;   // Assignment form
    Foo three;      // Default constructor
    Foo four();     // WRONG: a function declaration
    Foo five{};     // Uniform initialization
}

Note that uniform initialization has a preference of initializer_list as a type.

Members of a class can be initialized before the body of a constructor using a semicolon.

class Foo
{
    int bar, baz;
public
    Foo(int r, int z) : bar{r}, baz{z} {}
};

int main()
{
    Foo *foos = new Foo[2] {{1, 2}, {3, 4}};
}

Initializing a object without new allocates it within local scope and typically on the stack, which gets destroyed automatically when the local scope ends. In contrast, using new allocates a block of memory and returns an address that is stored in a pointer. The object is normally stored on the heap and requires a call to delete to be destroyed.

Note that struct acts the same as class, except that members of a struct default to public, while members of a class default to private. Similarly, a union is a class with members that default to public, but of course it can only store one data member at a time.

The keyword this is used within a class’s member functions to refer to the object itself.

A static member of a class is shared between all instances of that class. Since they have the same properties as non-member variables, but are within the scope of the class they must be initialized outside the class (e.g. int Foo::bar = 0;). Similarly, static member functions can not access non-static members of the class or this.

When an object of a class is declared as const its members are restricted to be read-only outside the object, but they still may be modified in the object and initialized by calling a constructor. Member functions in this case may only be called if they are specified as const (e.g. int foo() const {}). As a rule of thumb, const members shall not modify the state of an object. Two member functions with identical signatures may be declared if only one is specified as const.

It is possible to create class templates in much the same way as one can create function templates.

template <class T>
class Foo
{
    T bar, baz;
public:
    Foo(T r, T z) : bar{r}, baz{z} {}
};

Class templates may be overloaded depending on the type of template arguments it is passed. A template specialization is written in addition to the original class definition. For example, given the above implementation one could add the following:

template <>
class Foo <char>
{
    char bar, baz;
public:
    Foo(char r, char z) : bar{r}, baz{z} {}
};

Constructors

When a object of a class without a constructor is declared, but not initialized with any arguments (e.g. Foo foo;) then it’s implicit default constructor is called. It must be called explicitly (i.e. you need to write a constructor without arguments) when a constructor exists with one or more arguments.

Conversely, a destructor is called when an object is cleaned up (e.g. the object loses scope).

Copy constructors take a reference to the class itself (e.g. Foo::Foo(const Foo&);). The default copy constructor does a shallow copy, which involves just copying the class members. A deep copy, which may involve handling pointers so that both objects don’t hold a pointer to the same object, requires writing a custom copy constructor. When writing a custom copy constructor it is probably also worth overloading the copy assignment operator (e.g. Foo& operator= (const Foo&);).

Move constructors are called on unnamed objects that are typically return values or type-casts. Note that compilers use Return Value Optimization, which may mean the move constructor doesn’t get called (e.g. auto bar = foo();). Move constructors and assignments take a rvalue reference to the class. For example,

Foo (Foo&&);            // Move constructor
Foo operator= (Foo&&);  // Move assignment

Moving often means simply copying the pointer to an object that has been dynamically allocated to another object of the same type. For example,

Foo (Foo&& f) : bar(f.bar) {f.bar=nullptr;}

Friendship

A function that is declared in a class with the friend keyword may access the private and protected members of that class, but is not considered a member function.

A class that is included in the definition of another class with the friend keyword (e.g. friend class Foo) may similarly access the private and protected members of the other class.

Inheritance

For example, the derived class, Bar, inherits from the base class, Foo:

class Bar: public Foo {};

In this case public allows Bar to inherit all the members with the same levels they had in Foo. Alternatively, replacing it with protected means that all public members of Foo will be inherited as protected. Finally, private follows as one would expect. If a access level is not specified then classes default to private, while structs default to public.

The base class default constructor will be called unless otherwise specified.

class Bar : public Foo {
public:
    Bar(int a): Foo(a) {}
}

This example will call the corresponding constructor of the base class, Foo.

Multiple inheritance is also supported as follows:

class Bar : public Foo, public Baz;

Polymorphism

A pointer to a derived class is type-compatible with a pointer to its base class, but only members inherited from the base class can be accessed.

If the declaration of a member function in a base class is preceded with virtual then it can be redefined in a derived class. Both of the base class and derived class are referred to as polymorphic classes.

A base class which contains virtual functions without definitions (i.e. pure virtual functions) are referred to as abstract. It may only be used as a base class and therefore can not be used to instantiate an object. Note that the definition of the function is replaced by =0 (e.g. virtual int foo() =0;).

Conversion

Implicit conversions can be supported using three types of member functions:

class Foo {};

class Bar {
public:
    Bar (const Foo& foo) {}            // Constructor (e.g. Bar bar = foo)
    Bar operator= (const Foo& foo) {}  // Assignment (e.g. bar = foo)
    operator Foo() {}                  // Conversion (e.g. foo = bar)
}

Adding explicit to the beginning of a constructor declaration that performs a conversion prevents implicit conversions for arguments of any function that takes an argument of that type.

Exceptions

Custom exceptions should inherit from <stdexcept> or when necessary std::exception.

#include <exception>
#include <stdexcept>
#include <string>

class CustomError : public std::exception {
public:
    CustomError(std::string msg) : msg_(msg) {}
    ~CustomError() noexcept {}
    virtual const char* what() const noexcept {
        return msg_.c_str();
    }   
private:
    std::string msg_;
};

class CustomRuntimeError : public std::runtime_error {
public:
    CustomRuntimeError(std::string const &msg) : std::runtime_error(msg) {}
};

Other

Other topics of note:

Libraries

Resources