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