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.
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):
See Fluent C++ blog article on most vexing parse.// 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)
- 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).
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.void fun() noexcept; // function that is guaranteed not to throw
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
- 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
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.
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):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 }
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.
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.//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 }; }
This is called a contextual conversion ( Complex is contextually convertible).class Complex { public: explicit operator bool() const { return abs() != 0; } }; int main() { Complex c{2,3}; if ( c ) // c.operator bool called { ... } }
- 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 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:
- Initialize base classes (call direct base class constructors) in order (if several bases)
- Initialize data members in declaration order
- Destroy data members in reverse order
- Destroy base classes in reverse order
- Derived11 constructor called
- Derived1 constructor called
- Base constructor called
- Base::val initialized
- Base::pval initialized
- Derived1::d initialized
- Base constructor called
- Derived1 constructor called
- 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
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.Derived21 d; Base * bp { &d };
- 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:
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!void foo(Derived2 d){ } ... Derived22 d2{2,1}; foo(d2);
- 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} };
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:
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).try{ some_code_that_may_thow(); } catch ( std::exception const & e ) { cerr << e.what(); // print error message }
- 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:
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() { int * ptr { new int{} }; foo(); delete ptr; }
Now ptr will take care of deallocation of the allocated memory at destruction (end of scope or before exception handler).void fun() { unique_ptr<int> ptr { make_unique<int>(5) }; // unique_ptr<int> ptr { new int{5} }; foo(); }
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