Hide menu

TDDD38 Advanced Programming in C++

Seminar topics

This page lists topics covered during seminars, often with descriptions and links to code examples or extra reading material.

Seminar 1 (24/1) - Basic C++

Value categorization
Every expression is part of exactly one value category; lvalue, xvalue or prvalue [basic.lval]. xvalues denote something temporary, such as X{} (if X is a class), lvalues denote an object (the name rule state that everything that has a name is an lvalue) and a prvalue is a literal or the value of some expression (that can be used to initialize an lvalue or xvalue). See this blog for more discussion on value categorization.
Type conversions
There are a lot of type conversions done by the compiler (implicit conversions). Often in the form of integral promotions (something with less precision can be converted to int). In reality, there are a lot of implicit type conversions (specified in [conv]), here are a list of some important conversion:
array-to-pointer and function-to-pointer conversion
An l-value of array type or a function name decays to pointer at the first possibility
promotions (integral and floating)
Integral types smaller than int can be promoted into int implicitly. Floating point values are promoted to double.
integral and floating conversions
Conversions between integral values that aren't promotions are regarded a conversion. Uses integer conversion rank [conv.rank] to determine which conversion is preferred.
Boolean conversion
"A prvalue of arithmetic, unscoped enumeration, pointer, or pointer to member type can be converted to a prvalue of type bool." [conv.bool]. A zero value is false everything else is true.
Code example Basic_CPP/basic_if.cc show a problem with implicit type conversion.
Explicit type conversion is something that the programmer asks for. There are four types of explicit conversions (in modern C++). For simple type conversions, use static_cast. See http://stackoverflow.com/questions/332030/ for some discussion on the others. Please do not use C-style casts. Doing type conversions is ugly (tries to circumvent the static type system) and should look ugly. It is also very easy to search for in code.
Function overloading and simple parameter passing
A function can be overloaded (i.e. using the same name for many functions) as long as the parameter lists are different. You can't overload only on return type. See code example Basic_CPP/basic_overloading.cc for some examples. See CppCoreGuidelines F.call for a guideline on passing arguments
Most vexing parse
If an expression could be both a declaration and a definition, the declaration has to be preferred by the compiler. The following code example (also available as Basic_CPP/basic_mvp.cc) shows this (and some type conversions):
// Declaring a function taking a pointer to function (returning int, taking no arguments)
// as argument and returning int
int var (int());
cout << var << endl;  // Will print 1 (function name decays into function pointer,
                      //               which converts to bool)    
See Fluent C++ blog article on most vexing parse.
Command-line arguments
To use command-line arguments in C++, we add two arguments to main of type int and char * [] (i.e. array of c-string). See Basic_CPP/basic_cmd-args.cc for short example.
The noexcept specifier
Marking a function noexcept means that we promise that this function won't throw any exceptions. Please use it, both for the user of this function and to let the compiler do optimizations. If you throw (or let an exception leave the function scope) from a noexcept-function, the program will crash (std::terminate is called).
void fun() noexcept; // function that is guaranteed not to throw
Functions can be overriden with noexcept. You can have one function that is noexcept and one that isn't and the compiler will call the one needed.

Seminar 2 (Jan 31) - Class design

const
Marking a member function const will allow calls to this member function on a const object. Usually, all member functions could modify the object but a const function isn't allowed to. Your default should be to mark all member functions const as long as it doesn't modify your object.
void Cls::fun() const; // fun isn't allowed to modify members of Cls
Constructors
The main responsibility of any constructor is to initialize an object by initializing the data members. A constructor is a member function written with a special syntax, CLASS_NAME(arguments);. Unlike most other member functions, it doesn't have a return type and the name has to be the same as the class. Initialization is done in a mem-initializer-list [class.base.init], a comma separated list where each member is initialized in declaration order. A member can also have default values which is evaluated if it isn't initialized in the initializer list.
class Cls
{
public:
    Cls(int a)
        : val1{a}, val3{2} // member initializer list, val1 is initialized with a,
                           //                          val2 uses the default value (5) and
                           //                          val3 is set to 2
    { } // constructor function body left empty
private:
    int val1;
    int val2 {2+3};
    int val3 {4};
};
Special member functions
There are six special member functions that are used to take care of resources that your class is responsible for. Given class Cls;, here are the declarations for those;
Cls::Cls(); - Default constructor
A constructor that can be called without arguments. Should initialize your object with some good default value.
Cls::~Cls(); - Destructor
Called automatically when an object should disappear. There are two situations when this happens:
  • The scope where the object is defined ends (usually end of statement for xvalues and end of block for lvalues)
  • delete is called on a pointer to something that is dynamically allocated; the destructor is called and then the memory is deallocated
Cls::Cls(Cls const &); - Copy constructor
Used to create a new object as a copy of an existing object.
Cls & Cls::operator=(Cls const &); - Copy assignment operator
Used to reassign an existing object and make it a copy of the right hand side object (passed as argument). Usually returns a reference to lhs (*this)
Cls::Cls(Cls && other); - Move constructor
Binds an xvalue - used to move resources from something that is about to be destroyed (other) instead of creating a copy and then destroying it.
Cls & Cls::operator=(Cls && rhs); - Move assignment operator
Moves resources from xvalue rhs
Usually, the compiler can generate these for you, but there are some rules about this, here is a short version (see [class.copy] for exact rules):
  • Is there a declared (non-special) constructor (Cls(int) etc.)? => No default constructor generated
  • Is there a declared copy constructor or copy assignment operator? => No move operation generated
  • Is there a declared move constructor or move assignment operator? => No copy or move operation generated
Functions that aren't removed by these rules can still be removed for other reasons (often because of subobjects with missing special members - if your data member doesn't have a copy constructor the compiler will have a hard time to generate your copy constructor).
The compiler can generate copy and move operations if there is a destructor declared, but this is deprecated (can change in the future).
=default and =delete
If you want the compiler to generate a special member function that normally would be removed, you can mark it as defaulted. If you want to remove a generated function it can be marked as deleted. Only special member functions can be marked default, any function is allowed to be deleted. Please note that marking a function default or delete is declaring them and thus triggering rules for generating special member functions.
Cls(int);                  // removes generation of defult constructor
Cls() = default;           // generates it anyway
Cls(Cls const &) = delete; // removes copy constructor
Cls(Cls &&) = default;     // generates move constructor
Rule of N (with N=0, 3 or 5)
Pre-C++11, the concept of rule of three was common. The meaning of the rule of three is that if you require a destructor or copy operation with non-standard behavior to manage your resources, you probably need all three. With C++11 (and move semantics with it), this expanded to rule of five - if you need to manage your resources in some non-default way, you should probably think about all five. rule of zero is another take on this - if all your resources take care of their own data in a good way, you won't have to - let the compiler generate all special member functions!
RAII - Resource Acquisition Is Initialization
As long as you allocate (acquire) your resources in the constructor, take care of the lifetime of your object and release the resource in the destructor, the language will take care of the lifetime of your resources. If your class only has RAII-friendly members (that is classes that take care of their own resources in a good way), you don't have to care about those resources and can follow the rule of zero!
See code example raii where we take a mock C-based network library and encapsulate it in RAII-friendly classes.
Copy elision
The as if rule state that the compiler can do whatever it wants with our code (usually to make it as efficient as possible) as long as the observable behavior of the final program is the same as what is expressed in code. There is one exception to this rule and that is copy elision. The compiler is free to remove unnecessary calls to copy or move constructors even if they do have observable side effects. Therefore, you should never count on side effects from these functions. One special case is NRVO, named return value optimization, where the compiler can remove a lot of constructor calls. Three things has to be fulfilled for NRVO; having a function that returns an object by value, returning a local variable by name in that function and using the return value to initialize an object. The compiler is free to (required to according to C++17) use exactly one object in the entire call chain. The code example below is adapted from noisy.cc.
Cls fun()  // return by value (usually a copy is created)
{
    Cls c;
    c.do_stuff();
    return c;
}

int main()
{
    Cls cl { fun() };
    // ... continue with main
}
This code could (and in C++17 should) be expanded to something like this (the compiler could of course keep the function and use some reference technique instead):
int main()
{
    Cls cl;
    cl.do_stuff();
    // ... continue with main
}
Operator overloading
Almost all operators (. .* :: :? excluded) can be overloaded for your own types. For any binary operator @, the expression x@y will be evaluated as either as x.operator@(y) or operator@(x,y) (in that order). For full mapping between expression and matching function, see table 12 in [over.match.oper]. [over.oper] lists all available operators. Two basic rules of thumb when overloading operators:
  • Do I need this operator? - If it doesn't make sense logically to add two objects, don't provide that operator
  • When figuring out behavior- take inspiration from built in types, that behavior is the expected for most users. The user wouldn't expect either argument in a subtraction to be modified.
Type conversions
There are two ways of supporting type conversion for user-defined types;
Type converting constructor
A constructor that can take ONE argument of any other type can be used for type conversion by the compiler.
Type conversion function
A member function with the declaration Cls::opearator TO_TYPE() can be used to convert an object of type Cls to type TO_TYPE.
Both of these methods can be used implicitly by the compiler to do type conversions as needed. This can create a lot of instable code that is hard to reason about. To forbid implicit type conversion, a converting function can be marked explicit.
//simple class to represent a complex number
class Complex
{
public:
    Complex(double re, double im=0.0); // type converting, can be called with one double
    explicit operator double() const
    {
        return abs();
    }
    double abs() const;
};

int main()
{
    Complex c{2.3, 4.4};

    // the following requires a operator+(Complex, double) (would give compiler error here)
    //  if conversion function wasn't marked explicit, it would be called to convert
    //  c and add two values of type double
    Complex c2 { c + 2.0 };
    // if I really want that behavior, I have to explicitly ask for a type conversion:
    Complex c3 { static_cast<double>(c) + 2.0 };
}
A type converting function to bool can be called by the compiler implicitly even if it is marked explicit if it occurs at a location where a boolean value is expected by the language.

class Complex
{
public:
    explicit operator bool() const
    {
        return abs() != 0;
    }
};

int main()
{
    Complex c{2,3};
    if ( c )  // c.operator bool called
    {
        ...
    }
}
This is called a contextual conversion ( Complex is contextually convertible).
ref-qualifiers
You can limit calls to member functions based on value category. By adding one & to the function specification, you limit calls to lvalues. By adding two &, it can only be called on rvalues (xvalue or prvalue).
Cls & Cls::operator=(Cls const & c) &;  // can only assign to lvalues
...
Cls c;
Cls{} = c; // compiler error, lhs not lvalue

Seminar 3 (14/2) - Inheritance and Polymorphism

Many of the descriptions below uses the following class hierarchy (available as inheritance.cc).
class hierarchy
class Base
{
public:
    Base(int val, int pval): val{val}, pval{pval} {}
    virtual ~Base() = default;
    virtual void fun() const { }
    void foo() const
    {
        do_foo();
    }
    void bar() const {}
protected:
    int val;
private:
    virtual void do_foo() const = 0;
    int pval;
};

class Derived1: public Base
{
public:
    void fun() const override {}
    Derived1(int val, int pval, double d)
        : Base{val, pval}, d{d} {}

private:
    double d;
};

class Derived11 final: public Derived1
{
    using Derived1::Derived1;
    void do_foo() const override {}
};
class Derived2: public Base
{
    using Base::Base;
    void do_foo() const final {}
public:
    void bar() const {}
};
struct Derived21: Derived2
{
    using Derived2::Derived2;
    void fun() const override {}
};

Lifetime of objects in class hierarchies
An object is initialized in the following order:
  1. Initialize base classes (call direct base class constructors) in order (if several bases)
  2. Initialize data members in declaration order
At destruction, subobjects are destroyed in reverse order:
  1. Destroy data members in reverse order
  2. Destroy base classes in reverse order
For an object of type Derived11, this means the following initialization order:
  1. Derived11 constructor called
    1. Derived1 constructor called
      1. Base constructor called
        1. Base::val initialized
        2. Base::pval initialized
      2. Derived1::d initialized
access to members
There are three access levels in classes;
public
Accessible for anyone.
private
Only accessible to this class (such as member functions).
protected
Accessible for subclasses (and this class), but hidden from others. This access level could be nice, but please don't overuse it (makes code hard to maintain).
Types of inheritance
The keywords public, protected and private can also used to represent type of inheritance. With public inheritance, all public members in the base class will be public in the derived class while protected members will be kept protected. protected inheritance means that all available members from the base class (public and protected) will be protected in the derived class and private inheritance leads to only private members from the base class. For classes, the default (if left out) is private inheritance, for structs the default is public.
Polymorphism
To get polymorphic behavior in C++, there are two requirements;
  • Using a pointer or reference to a base class
  • Calling a function (declared in base class) that is marked virtual
Assume the following declarations:
Derived21 d;
Base * bp { &d };
The call bp->bar() will not get dynamic dispatch and will call bar in Base since the function isn't declared virtual. The call bp->fun() would trigger a polymorphic call and call the implementation in Derived21. See also "static vs dynamic type" below.
Slicing
Forgetting to use pointer or references when using polymorphic classes can lead to slicing. Slicing happen when you try to store an object of derived class in an object of base class. Short example:
void foo(Derived2 d){

}
...
Derived22 d2{2,1};
foo(d2);
Now the Derived2 subobject (part) of d2 will be copied to d instead of the entire d2 object. This is called slicing. Sometimes this is what you want, but usually it is a bug - pass by reference!
Static vs. dynamic type
When working with polymorphic class hierarchies, it's important to keep track of a pointers static and dynamic types. The static type is the declared type of the pointer (Base * for bp above) and this will never change. The dynamic type is the type of the object that the pointer currently points to (most derived class). When figuring out which function to call, the compiler starts by looking at the static type. If the function exists in the static type and is declared virtual, we'll get dynamic dispatch to the implementation that is "closest" to the dynamic type. If the function doesn't exist in the static type, the code won't compile.
override
The override keyword (actually it's an identifier with special meaning) was added in C++11 to show your intent better. By using override where you intend to override a virtual function, the compiler can check that you do it correctly. It is of course also helpful for other readers of your code.
final
The final keyword (also identifier with special meaning) can be used on classes or functions to forbid further derivation or overriding respectively. This keyword should be used with care since it can hinder future development. It can be used together with override, but the recommendation is to use one or the other. Only virtual functions can be overridden so final implies override.
virtual destructor
If you have a polymorphic class hierarchy and you expect your user to allocate objects dynamically, you should (must) always provide a virtual destructor in the base class. Otherwise, calling delete on a pointer to base class will only call the destructor for the base class, not the destructor for the current dynamic type. This is undefined behavior. See also CppCoreGuidelines C.35
dynamic_cast
To get the dynamic type, we use RTTI (RuntTime Type Information). Usually we want to get access to some specific member that is available in a subset of our class hierarchy and to do this, we need a cast. dynamic_cast works with pointers or references in class hierarchies as seen below:
  • Pointers: DerivedX * dp = dynamic_cast<DerivedX*>(basePtr). We try to convert pointer to base class basePtr into pointer to derived class DerivedX. If the dynamic type of basePtr is DerivedX (or subclass thereof), the call returns a pointer value, otherwise nullptr.
  • References: DerivedX & dr = dynamic_cast<DerivedX&>(baseRef). Works in similar way as for pointers, but since we don't have null references, a std::bad_cast will be thrown if the dynamic type of baseRef isn't (possible subclass of) DerivedX.
using-declaration
A using-declaration [namespace.udecl] can be used to bring members from base classes into the current scope. When used to name a constructor, constructors matching the base class constructors will be created in the current class. The created constructors will accept the same arguments and forwards those to the corresponding base class constructor (see [class.inhctor.init]). The access level of those constructor will be the same as in the base class. Derived11 will get a constructor that accepts two int parameters and one double and passes those to the Derived1 constructor. using-declarations can also be used on normal member functions. The access level of the imported functions will be decided by the subclass (where the using declaration is made). See example below:
struct B{
  void fun() {}
  void fun(int) {}
  void fun(double) {}
protected:
  void fun(char) {}
private:
  void fun(int, int) {}
};
struct D: B {
  void fun(char, int) {}   // hides base class overloads
  using B::fun;            // imports public and protected overloads as if they were declared here
  void fun(int) = delete;  // remove this overload
};
...
B b;
b.fun();    // ok
b.fun('a'); // ok
b.fun(1,3); // NOT ok, private in B
b.fun(3);   // NOT ok, explicitly deleted
    
vtable (virtual table)
The lookup of correct implementation of virtual functions is usually done through a virtual table. Each class (not object) has a table of function pointers referring to the implementation to be used for the current class. The compiler then injects a vtable pointer into each base class object which is then updated with the correct virtual table according to the dynamic type. See the figure below for an example given the following declaration:
Base * bp { new Derived11{1,2,3.4} };

virtual table example
It is important to remember that polymorphism isn't free. It requires both space and time (dynamic dispatch through pointers is a huge bottleneck). You should only use it if it makes your code easier to reason about or better in any other way. Don't use it if you don't need to.
The zero-overhead rule state that "what you don't use, you won't pay for". This is one reason why we don't get polymorphism automatically (we need to use virtual functions).
Exception handling
Error passing in C++ is usually done using exceptions. You can throw whatever you want, but the language and standard library throws objects derived from std::exception. If you want to throw exceptions, select a fitting class (from <stdexcept>) and throw it by value (such as throw std::logic_error{"Error message"};). You catch exceptions with try ... catch:
try{
    some_code_that_may_thow();
}
catch ( std::exception const & e )
{
   cerr << e.what(); // print error message
}
Since the built-in exception classes are part of a class hierarchy, you want to catch by reference (to avoid slicing). Remember to catch exceptions. If an exception escapes the main function, std::terminate is called and the stack isn't unwound (no destructors are called).
Smart pointers
Using raw pointers for representing ownership is not a good idea. It makes your code less expressive and requires a lot of extra work. Look at the following example:
void fun()
{
    int * ptr { new int{} };
    foo();
    delete ptr;
}
This looks good, but what happens if foo throws? Then we have a memory leak! C++11 added smart pointer types. There are two owning smart pointers, std::unique_ptr to represent unique ownership and std::shared_ptr to represent shared ownership.
void fun()
{
    unique_ptr<int> ptr { make_unique<int>(5) };
    // unique_ptr<int> ptr { new int{5} };
    foo();
}
Now ptr will take care of deallocation of the allocated memory at destruction (end of scope or before exception handler).
A good rule of thumb is "is there a delete in the code" -> something is probably bad, move the resource to a better handler (such as smart pointer). The exception has to be caught somewhere else in the call chain.
Non-Virtual Interface NVI
The non-virtual interface pattern means that you declare a virtual function private in your base class and then call it from a public (non-virtual) function in the base. See Base::foo. A reason for doing this is that it gives us a good location to do pre and post operations that we want to do for all implementations of do_foo.

Page responsible: Christoffer Holm
Last updated: 2018-02-21