Modern C++ Workshop
Day 2 - Classes
Agenda
constructor delegation
rule of five
default keyword
delete keyword
final and override
initializer lists
uniform initialization
non-static data member initialization
construction/destruction order
Constructor Delegation
class SomeType {
int number;
std::string name;
public:
SomeType(int number, std::string name)
: number(number)
, name(name)
{}
SomeType(int number)
: SomeType(number, "foo")
{}
SomeType(std::string name)
: SomeType(42, name)
{}
SomeType()
: SomeType(42, "foo")
{}
};
Non-modern C++ did not allow constructors to call each other.
Starting with C++11, constructors can delegate calls to other constructors inside the same class.
In a corresponding exercise, you'll use this to refactor code.
C++98 revisit:
special methods
#include <iostream>
struct A {
// constructor
A()
: name("hello")
{
std::cout << "constructor";
}
// destructor
~A() {
std::cout << "destructor";
}
// copy constructor
A(const A& other)
: name(other.name)
{
std::cout << "copy constructor";
}
// copy assignment operator
A& operator= (const A& other) {
name = other.name;
std::cout << "copy assignment";
return *this;
}
std::string name;
};
int main(int argc, char** argv) {
A var1;
A var2 = var1;
var1 = var2;
}
C++98 revisit:
special methods
#include <iostream>
struct A {
// constructor
A()
: name("hello")
{
std::cout << "constructor";
}
// destructor
~A() {
std::cout << "destructor";
}
// copy constructor
A(const A& other)
: name(other.name)
{
std::cout << "copy constructor";
}
// copy assignment operator
A& operator= (const A& other) {
name = other.name;
std::cout << "copy assignment";
return *this;
}
std::string name;
};
int main(int argc, char** argv) {
A var1;
A var2 = var1;
var1 = var2;
}
// output:
// constructor
// copy constructor
// copy assignment
// destructor
// destructor
Compiler generated methods
Default constructors initialize each member in order of declaration using their default constructor.
Default destructors destruct members in reverse order of their declaration.
Default copy constructors, copy assignment operators, move constructors and move assignment operators perform member-wise copy/move, respectively.
pre C++11: Rule of Three
If you define one of the following, you probably need to define all three:
- destructor
- copy constructor
- copy assignment operator
C++11: Rule of Five
If you define one of the following, you probably need to define all three:
- destructor
- copy constructor
- copy assignment operator
- move constructor
- move assignment operator
Default Keyword
struct Interface {
// compiler generated
// virtual destructor
virtual ~Interface() = default;
virtual void method() = 0;
};
class POD {
public:
// compiler generated default constructor
// this is needed because
// defining another constructor
// causes this one not to be generated
POD() = default;
POD(const std::string& name)
: name(name)
{}
private:
std::string name;
}
Default-generated methods are guaranteed to be inlineable by compilers (performance gains).
Delete Keyword
// stolen from boost
struct noncopyable {
noncopyable() = default;
~noncopyable() = default;
noncopyable(
const noncopyable&) = delete;
noncopyable& operator=(
const noncopyable&) = delete;
}
Inheritance vs Composition
"Program to an 'interface', not an 'implementation'."
(page 18)
Composition over inheritance: "Favor 'object composition' over 'class inheritance'."
(page 20)
Final Keyword
struct Base
{
virtual void foo();
};
struct A : Base
{
// A::foo is final
void foo() final;
// Error: non-virtual function cannot be final
void bar() final;
};
// struct B is final
struct B final : A
{
// Error: foo cannot be overridden as it's final in A
void foo();
};
// Error: B is final
struct C : B
{
};
1. used in classes to prevent inheritance
2. used in methods to prevent overloading by derived classes
Override Keyword
struct A
{
virtual void foo();
void bar();
};
struct B : A
{
// Error: B::foo does not override A::foo
void foo() const final override;
// OK: B::foo overrides A::foo
void foo() final override;
// Error: A::bar is not virtual
void bar() override;
};
override is used to statically ensure that a method is really overriding a base classes virtual method
std::initializer_list
std::vector<int> vec = {1, 2, -4, 12};
for(const auto i : {1, 6, 12, -8, 15}) {
std::cout << i << std::endl;
}
template <typename T>
class MyContainer {
MyContainer(std::initializer_list<T> t) {
for(const auto& element : t) {
// do something with element
}
}
}
initialization
widget w; // (a)
widget w(); // (b)
widget w{}; // (c)
widget w(x); // (d)
widget w{x}; // (e)
widget w = x; // (f)
widget w = {x}; // (g)
auto w = x; // (h)
auto w = widget{x}; // (i)
What is the difference between those?
initialization
widget w; // (a)
w is constructed using the default constructor of class widget
or
w is uninitialized iff widget is a built in type
initialization
widget w(); // (b)
widget w{}; // (c)
(b) is actually a function declaration
(c) was introduced in C++11 and guarantees the default constructor is called
(even if there is a constructor taking std::initializer_list as an argument)
initialization
widget w(x); // (d)
widget w{x}; // (e)
(d) and (e) initialize widget by calling its constructor, passing x
If x is a type, (d) is actually a function declaration, even if there is a variable x in scope
(e) is never a function declaration
initialization
widget w(x); // (d)
widget w{x}; // (e)
(e) avoids narrowing:
struct A {
A(int n) : number(n) {}
int number;
}
// ok, implicit conversion
A one(1.23);
// compiler error, narrowing
A two{1.23};
initialization
widget w = x; // (f)
widget w = {x}; // (g)
(f) is copy initialization, (g) is copy list initialization no assignment operator is involved
The same reasons apply to prefer (g) over (f) as with (e) over (d)
If x is not of type widget, implicit conversions can occur with (f), but not with (g).
initialization
auto w = x; // (h)
auto w = widget{x}; // (i)
semantics are the same as with (f) and (g), but there are less ways to shoot yourself in the foot
(h) never converts types, guaranteed to only call a single copy constructor
(i) allows you to commit to a type, but avoid narrowing conversions
initialization
widget w; // (a)
widget w(); // (b)
// prefer this
widget w{}; // (c)
widget w(x); // (d)
widget w{x}; // (e)
widget w = x; // (f)
widget w = {x}; // (g)
// prefer these
auto w = x; // (h)
auto w = widget{x}; // (i)
non-static data member initialization
struct A {
A() = default;
A(const std::string& name)
: name(name)
{}
std::string name = "42";
}
A one;
auto two = A{"hello"};
std::cout << one.name << std::endl;
std::cout << two.name << std::endl;
construction order
- all virtual base classes in type hierarchy depth-first, left-to-right
- constructors execute non-virtual base class constructors first, left to right order
- members are initialized in declaration order, initializer list or non-static member defaults, whichever applicable
- the constructor body is executed
construction order
struct A1{};
struct A2{};
struct A : A2, A1{};
struct B {};
struct D : A, B {
D()
: name("foo")
, a({13})
{
a = {11};
name = "bar";
}
std::string name
= "abc";
std::vector<int> a
= {3};
};
Whiteboard exercise: name all called constructors and their arguments in the right order.
- all virtual base classes in type hierarchy depth-first, left-to-right
- constructors execute non-virtual base class constructors first, left to right order
- members are initialized in declaration order, initializer list or non-static member defaults, whichever applicable
- the constructor body is executed
Thanks,
Questions?
Modern C++ Workshop - Classes
By Jupp Müller
Modern C++ Workshop - Classes
- 915