0% found this document useful (0 votes)
8K views103 pages

Timur Doumler - Initialisation in Modern C++ - Cpponsea2019

The document discusses different ways to initialize objects in C++, including default, copy, aggregate, static, and direct initialization. It provides examples and explanations of each technique and how they have evolved in newer C++ standards from C to C++17. The document also notes some issues like the most vexing parse that can arise with direct initialization.

Uploaded by

jaansegus
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
8K views103 pages

Timur Doumler - Initialisation in Modern C++ - Cpponsea2019

The document discusses different ways to initialize objects in C++, including default, copy, aggregate, static, and direct initialization. It provides examples and explanations of each technique and how they have evolved in newer C++ standards from C to C++17. The document also notes some issues like the most vexing parse that can arise with direct initialization.

Uploaded by

jaansegus
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
You are on page 1/ 103

Initialisation

in modern C++
Version 1.1

Timur Doumler

@timur_audio

CppOnSea
5 February 2019
https://imgur.com/3wlxtI0 !2
!3
This talk

• Different ways to initialise an object in C++


• In order of introduction: C, C++98, C++03, C++11, C++14, C++17
• The future: C++20
• Recommendations
• Overview table (updated!)

!5
!6
Default initialisation

int main() {
int i;
}

!7
Default initialisation

int main() {
int i;
return i; // Undefined behaviour!
}

!8
Default initialisation

struct Foo {
int i;
int j;
};

int main() {
Foo foo;
return foo.i; // Undefined behaviour!

!9
Default initialisation

class Foo {
public:
Foo() {}
int get_i() const noexcept { return i; }
int get_j() const noexcept { return j; }

private:
int i;
int j;
};

int main() {
Foo foo;
return foo.get_i(); // Undefined behaviour!
}

!10
C++98: member initialiser list

class Foo {
public:
Foo() : i(0), j(0) {} // member initialiser list
int get_i() const noexcept { return i; }
int get_j() const noexcept { return j; }

private:
int i;
int j;
};

int main() {
Foo foo;
return foo.get_i();
}

!11
C++11: default member initialisers

class Foo {
public:
Foo() {}
int get_i() const noexcept { return i; }
int get_j() const noexcept { return j; }

private:
int i = 0; // default member initialisers
int j = 0;
};

int main() {
Foo foo;
return foo.get_i();
}

!12
Copy initialisation

int main() {
int i = 2;
}

!13
Copy initialisation

int main() {
int i = 2;
}

int square(int i) {
return i * i;
}

!14
Copy initialisation

int main() {
int i = 2;
• Initialiser starting with `=`, or
} • Passing argument by value, or
int square(int i) { • Returning by value
return i * i;
}

!15
Copy initialisation

int main() {
int i = 2;
• Initialiser starting with `=`, or
} • Passing argument by value, or
int square(int i) { • Returning by value

return i * i;
}
• Copy init is never an assignment

!16
Copy initialisation

int main() {
int i = 2;
• Initialiser starting with `=`, or
} • Passing argument by value, or
int square(int i) { • Returning by value

return i * i;
}
• Copy init is never an assignment
• If types don’t match, copy init
performs a conversion sequence

!17
Aggregate initialisation

int i[4] = {0, 1, 2, 3};

!18
Aggregate initialisation

int i[4] = {0, 1, 2, 3};


int j[] = {0, 1, 2, 3}; // array size deduction

!19
Aggregate initialisation

int i[4] = {0, 1, 2, 3};


int j[] = {0, 1, 2, 3}; // array size deduction

struct Foo { // aggregate type


int i;
float j;
};

Foo foo = {1, 3.14159};

!20
Aggregate initialisation

int i[4] = {0, 1, 2, 3};


int j[] = {0, 1, 2, 3}; // array size deduction

struct Foo { // aggregate type


int i;
float j;
};

Foo foo = {1, 3.14159}; // foo is aggregate-initialised;


// foo.i and foo.j are copy-initialised

!21
Zero initialisation of aggregate elements

struct Foo {
int i;
int j;
};

int main() {
Foo foo = {1};
return foo.j;
}

!22
Zero initialisation of aggregate elements

struct Foo {
int i;
int j;
};

int main() {
Foo foo = {1}; // elements with no initialiser are zero-initialised!
return foo.j; // OK, returns 0
}

!23
Zero initialisation of aggregate elements

struct Foo {
int i;
int j;
};

int main() {
Foo foo = {1}; // elements with no initialiser are zero-initialised!
return foo.j; // OK, returns 0
}

int arr[100] = {}; // all elements are zero-initialised!

!24
Brace elision

struct Foo {
int i;
int j;
};

struct Bar {
Foo f;
int k;
};

int main() {
Bar b = {1, 2};
return b.k; // What does this return?
}

!25
Brace elision

struct Foo {
int i;
int j;
};

struct Bar {
Foo f;
int k;
};

int main() {
Bar b = {1, 2}; // Equivalent to Bar b = {{1, 2}, 0};
return b.k; // returns 0!
}

!26
Static initialisation

static int i = 3; // Constant initialisation

!27
Static initialisation

static int i = 3; // Constant initialisation


static int j; // Zero-initialisation

!28
Static initialisation

static int i = 3; // Constant initialisation


static int j; // Zero-initialisation

int main()
{
return i + j; // OK, returns 3
}

!29
Initialisation order fiasco

static Colour red = {255, 0, 0}; // Uh-oh :(

!30
Initialisation order fiasco

static Colour red = {255, 0, 0}; // if constructor is constexpr (C++11)


// -> constant initialisation :)

!31
What have we got so far?

• Default initialisation (no initialiser)


• built-in types: uninitialised, UB on access
• class types: default constructor
• Copy initialisation (`= value`, pass-by-value, return-by-value)
• Aggregate initialisation (`= {args}`)
• Elements without initialisers undergo zero initialisation
• Static initialisation
• zero-initialisation by default
• constant initialisation (`= constexpr`)
!32
Foo foo(1, 2); // C++ introduces constructors!

!34
Foo foo(1, 2);
int i(3);

!35
Direct initialisation

Foo foo(1, 2);


int i(3);

!36
Direct initialisation

Foo foo(1, 2);


int i(3);

“whenever the initialiser is an argument list in parens”

!37
Direct initialisation

Foo foo(1, 2);


int i(3);

• Differences to copy initialisation:


• For built-in types: no difference
• For class types:
• Can take more than one argument
• Does not perform “conversion sequence”,

instead just calls constructor using normal overload resolution

!38
Direct initialisation

struct Foo {
explicit Foo(int) {}
};

Foo foo1 = 1;   // ERROR


Foo foo2(2);    // ok

!39
Direct initialisation

struct Foo {
explicit Foo(int) {}
Foo(double) {}
};

Foo foo1 = 1; // calls Foo(double)


Foo foo2(2); // calls Foo(int)

!40
Direct initialisation

Foo(1, 2); // constructor call notation


auto* foo_ptr = new Foo(2, 3); // new-expr with (args)
static_cast<Foo>(bar); // casts

!41
Problem: most vexing parse

struct Foo {};

struct Bar {
Bar(Foo) {}
};

int main() {
Bar bar(Foo());
}

!42
Problem: most vexing parse

struct Foo {};

struct Bar {
Bar(Foo) {}
};

int main() {
Bar bar(Foo()); // This is a function declaration :(
}

!43
What have we got so far?

• Default initialisation (no initialiser)


• Copy initialisation (`= value`, pass-by-value, return-by-value)
• Aggregate initialisation (`= {args}`)
• Static initialisation
• Direct initialisation (argument list in parens)
• Problem: most vexing parse

!44
03
Value initialisation

int main() {
return int();
}

!46
Value initialisation

int main() {
return int(); // UB in C++98, OK since C++03
}

!47
Value initialisation

int main() {
return int(); // UB in C++98, OK since C++03
}

“whenever the initialiser is a pair of empty parens”

!48
Value initialisation

When the initialiser is a pair of empty parens:


• If type has a user-provided default c’tor, it is called
• Otherwise, you get zero initialisation

!49
Value initialisation

struct Foo {
int i;
};

Foo get_foo() {
return Foo();
}

int main() {
return get_foo().i;
}

!50
Value initialisation

struct Foo {
int i;
};

Foo get_foo() {
return Foo(); // Value initialisation
}

int main() {
return get_foo().i; // OK since C++03, returns 0
}

!51
Value initialisation

struct Foo {
Foo() {} // user-provided ctor!
int i;
};

Foo get_foo() {
return Foo(); // Value initialisation
}

int main() {
return get_foo().i; // value is uninitialised -> UB!!!
}

!52
Value initialisation

struct Foo {
Foo() = default; // (since C++11) user-defined, but not user-provided
int i;
};

Foo get_foo() {
return Foo(); // Value initialisation
}

int main() {
return get_foo().i; // OK, returns 0
}

!53
Value initialisation

struct Foo {
Foo();
int i;
};

Foo::Foo() = default; // out-of-line counts as user-provided!

Foo get_foo() {
return Foo(); // Value initialisation
}

int main() {
return get_foo().i; // value is uninitialised -> UB!!!
}

!54
What have we got so far?
• Default initialisation (no initialiser)
• Copy initialisation (`= value`, pass-by-value, return-by-value)
• Aggregate initialisation (`= {args}`)
• Static initialisation
• Direct initialisation (argument list in parens)
• Value initialisation (empty parens)
• Performs default-init or zero-init
• Problem: most vexing parse

!55
++11
“Uniform initialisation”

• We’ve got too many different initialisation syntaxes


• Parens are vexing
• We cannot do:

std::vector<int> vec = {0, 1, 2, 3, 4};

• Instead we have to do:


std::vector<int> vec;
vec.reserve(5);
vec.push_back(0);
vec.push_back(1);
vec.push_back(2);
vec.push_back(3);
vec.push_back(4); !57
“Uniform initialisation”

• So let’s add one more initialisation syntax!


Foo foo{1, 2};

• It’s called list-initialisation


• Idea: it does “everything”

!58
List initialisation

Direct-list-initialisation Copy-list-initialisation
Foo foo{1, 2}; Foo foo = {1, 2};

!59
List initialisation

Direct-list-initialisation Copy-list-initialisation
Foo foo{1, 2}; Foo foo = {1, 2};

braced-init-list

!60
How does this work?

std::vector<int> vec{0, 1, 2, 3, 4};

!61
std::initializer_list

template <typename T>


class vector {
// stuff...
vector(std::initializer_list<T> init);  // init list ctor
};

std::vector<int> vec{0, 1, 2, 3, 4}; // calls that^

!62
!63
!64
std::initializer_list

std::vector<int> v(3, 0); // vector contains 0, 0, 0


std::vector<int> v{3, 0}; // vector contains 3, 0

!65
std::initializer_list

std::string s(48, 'a'); // "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"


std::string s{48, 'a'};

!66
std::initializer_list

std::string s(48, 'a'); // "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"


std::string s{48, 'a'}; // "0a"

!67
std::initializer_list

template <typename T, size_t N>


auto test() {
return std::vector<T>{N};
}

int main() {
return test<std::string, 3>().size(); // what does this return??
}

!68
List initialisation
• For aggregate types:
• aggregate init
• For built-in types:
• `{a}` is direct init, `= {a}` is copy-init
• For class types:
• First, greedily try to call a ctor that takes a std::initializer_list
• If there is none: direct-init 

(or copy-init if `= {a}` and a is a single element)
!69
Empty braces {} are special
• For aggregate types: aggregate init (all elements zeroed)
• Only call std::initializer_list ctor if there is no default ctor

template <typename T>


struct Foo {
Foo();
Foo(std::initializer_list<T>);
};

int main() {
Foo<int> foo{}; // calls default ctor!
}

!70
Empty braces {} are special
• For aggregate types: aggregate init (all elements zeroed)
• Only call std::initializer_list ctor if there is no default ctor
• Otherwise: value initialisation

!71
Empty braces {} are special
• For aggregate types: aggregate init (all elements zeroed)
• Only call std::initializer_list ctor if there is no default ctor
• Otherwise: value initialisation
struct Foo {
Foo() = default;
int i;
};

int main() {
Foo foo{}; // value init -> zero init, no vexing parse!
return foo.i; // returns 0
}

!72
Empty braces {} are special
• For aggregate types: aggregate init (all elements zeroed)
• Only call std::initializer_list ctor if there is no default ctor
• Otherwise: value initialisation
struct Foo {
Foo() {} // user-provided ctor!
int i;
};

int main() {
Foo foo{}; // value init -> default ctor gets called
return foo.i; // uninitialised! UB
}

!73
List init: no narrowing conversions!

int main() {
int i{2.0}; // Error!
}

!74
List init: nested braces

• The nice case:


std::map<std::string, int> my_map {{"abc", 0}, {"def", 1}}; 


!75
List init: nested braces

• The nice case:


std::map<std::string, int> my_map {{"abc", 0}, {"def", 1}}; 


• The evil case:


std::vector<std::string> v1 {"abc", "def"}; // OK
std::vector<std::string> v2 {{"abc", "def"}}; // ???

!76
List init: nested braces

• The nice case:


std::map<std::string, int> my_map {{"abc", 0}, {"def", 1}}; 


• The evil case:


std::vector<std::string> v1 {"abc", "def"}; // OK
std::vector<std::string> v2 {{"abc", "def"}}; // Undefined behaviour!!!

!77
Copy list init

• The nice case:


std::map<std::string, int> my_map {{"abc", 0}, {"def", 1}}; 


• The evil case:


std::vector<std::string> v1 {"abc", "def"}; // OK
std::vector<std::string> v2 {{"abc", "def"}}; // Undefined behaviour!!!

!78
Copy list init

Widget<int> f1()
{
return {3, 0}; // copy-list init
}

void f2(Widget);
f2({3, 0}); // copy-list init

!79
!80
What have we got so far?
• Default initialisation (no initialiser)
• Copy initialisation (`= value`, pass-by-value, return-by-value)
• Aggregate initialisation (`= {args}`)
• Static initialisation
• Direct initialisation (argument list in parens)
• Value initialisation (empty parens)
• List initialisation (`{args}` is direct-list-init, `= {args}` is copy-list-init)
• Performs aggregate-init or direct-init or copy-init or value-init
• Problems with std::initializer_list, useless in templates
!81
14
Fix #1: aggregates can have DMIs

struct Foo {
int i = 0;
int j = 0;
};

Foo foo{1, 2}; // OK since C++14

!83
Fix #2: auto + list-initialisation

int i = 3; // int
int i(3); // int
int i{3}; // int
int i = {3}; // int

auto i = 3; // int
auto i(3); // int
auto i{3}; // std::initializer_list<int> in C++11
auto i = {3}; // std::initializer_list<int> in C++11

!84
Fix #2: auto + list-initialisation

int i = 3; // int
int i(3); // int
int i{3}; // int
int i = {3}; // int

auto i = 3; // int
auto i(3); // int
auto i{3}; // int since C++14, ill-formed if more than one initialiser
auto i = {3}; // std::initializer_list<int> (always)

!85
17
Guaranteed copy elision

auto num = 1;
auto foo = Foo{2, 3};

!87
Guaranteed copy elision

auto foo = std::atomic<int>{0}; // C++11/14: Error


// atomic is neither copyable nor movable

!88
Guaranteed copy elision

auto foo = std::atomic<int>{0}; // Works in C++17 :)

!89
Guaranteed copy elision

auto foo = std::atomic<int>{0}; // Works in C++17 :)

“Almost always auto” is now “Always auto” !! :)

!90
Initialisation and CTAD

!91
20
!92
Designated initialisation

struct Foo {
int a;
int b;
int c;
};

int main() {
Foo foo{.a = 3, .c = 7};
}

!93
Designated initialisation

struct Foo { Only for aggregate types.


int a;
C compatibility feature.
int b;
int c; Works like in C99, except:
};
• not out-of-order 

int main() { Foo foo{.c = 7, .a = 3} // Error
Foo foo{.a = 3, .c = 7}; • not nested 

}
Foo foo{.c.e = 7} // Error
• not mixed with regular initialisers

Foo foo{.a = 3, 7} // Error
• not with arrays 

int arr[3]{.[1] = 7} // Error

!94
Array size deduction in new-expressions
http://wg21.link/p1009

double a[]{1,2,3}; // OK
double* p = new double[]{1,2,3}; // Error in C++17, will be OK in C++20

!95
Aggregates can no longer declare constructors
http://wg21.link/p1008

struct Foo {
Foo() = delete;
int i;
int j;
};

Foo foo1; // Error


Foo foo2{}; // OK in C++17! Will be error in C++20

!96
Problems with list init:

• Difficult to see when it’ll call a std::initializer_list constructor,



and when it won’t

• std::initializer_list doesn’t work with move-only types

• Useless in templates

(you can’t write a make_unique that works for aggregates!)


• Does not work with macros at all:


assert(Foo{2, 3}); // This breaks the preprocessor :(

!97
Aggregate initialisation from parens
http://wg21.link/p0960

struct Foo {
int i;
int j;
};

Foo foo(1, 2); // will work in C++20!

!98
Aggregate initialisation from parens
http://wg21.link/p0960

struct Foo {
int i;
int j;
};

Foo foo(1, 2); // will work in C++20!


int arr[3](0, 1, 2); // will work in C++20!

!99
Aggregate initialisation from parens
http://wg21.link/p0960

struct Foo {
int i;
int j;
};

Foo foo(1, 2); // will work in C++20!


int arr[3](0, 1, 2); // will work in C++20!

Idea: in C++20, () and {} will do the same thing!


Except:
• () does not call std::initializer_list constructors
• {} does not consider narrowing conversions
!100
Recommendations:

• Use auto
• Use direct member initialisers (DMIs)
• Use `= value` for int and other simple value types
• Use `= {args}` for aggregate-init, std::initializer_list, DMIs

→ Recommendation for aggregates might change for C++20!)
• Use `{}` for value-init
• Use `(args)` to call constructors that take arguments

→ This is the controversial one. Other people say: use `{args}`
!101
Initialisation in C++17 Version 2 – Copyright (c) 2019 Timur Doumler

Default init Copy init Direct init Value init Empty braces Direct list init Copy list init
Type var ; = value; (args); (); {}; = {}; {args}; = {args};
Built-in types Uninitialised. Initialised with 1 arg: Init with arg Zero-initialised Zero-initialised 1 arg: Init with arg 1 arg: Init with arg
Variables w/ static value (via conver- >1 arg: >1 arg: >1 arg:
storage duration: sion sequence) Doesn’t compile Doesn’t compile Doesn’t compile
Zero-initialised
auto Doesn’t compile Initialised with Initialised with Doesn’t compile Doesn’t compile 1 arg: Init with arg Object of type
value value >1 arg: std::initializer_list
Doesn’t compile
Aggregates Uninitialised. Doesn’t compile Doesn’t compile Zero-initialised*** Aggregate init** 1 arg: implicit 1 arg: implicit copy/
Variables w/ static (but will in C++20) copy/move ctor if move ctor if
storage duration: possible. Otherwise possible. Otherwise
Zero-initialised*** aggregate init** aggregate init**
Types with Default ctor Matching ctor (via Matching ctor Default ctor Default ctor if there std::initializer_list std::initializer_list
std::initializer_list conversion is one, otherwise ctor if possible, ctor if possible,
ctor sequence), explicit std::initializer_list otherwise matching otherwise matching
ctors not considered ctor ctor ctor****
Other types with Members are Matching ctor (via Matching ctor Zero-initialised*** Zero-initialised*** Matching ctor Matching ctor****
no user-provided* default-initialised conversion
default ctor sequence), explicit
ctors not considered
Other types Default ctor Matching ctor (via Matching ctor Default ctor Default ctor Matching ctor Matching ctor****
conversion
sequence), explicit *not user-provided = not user-declared, or user-declared as =default inside the class definition
ctors not considered **Aggregate init copy-inits all elements with given initialiser, or value-inits them if no initialiser given
***Zero initialisation zero-initialises all elements and initialises all padding to zero bits
****Copy-list-initialisation considers explicit ctors, too, but doesn’t compile if such a ctor is selected
Thank you!

@timur_audio
includecpp.org
103

You might also like