Show Lecture.Constructors as a slide show.
CS253 Constructors
Uniform Initialization Syntax
There are a variety of constructor (“ctor”) syntaxes, but they
all call a ctor, even the first one :
string a;
// string b(); 🦡
string c{};
string d(4, 'I');
string e("five");
string f{"six"};
string g = "seven";
string h{'e','i','g','h','t'};
string i = {'n','i','n','e'};
cout << "a=" << a << '\n'
<< "c=" << c << '\n'
<< "d=" << d << '\n'
<< "e=" << e << '\n'
<< "f=" << f << '\n'
<< "g=" << g << '\n'
<< "h=" << h << '\n'
<< "i=" << i << '\n';
a=
c=
d=IIII
e=five
f=six
g=seven
h=eight
i=nine
Syntax
Since this is Uniform Initialization Syntax,
it also applies to non-objects:
int a(1);
// int b(); 🦡
int c{3};
int d = 4;
int e[]{1,2,3,4,5};
int f[] = {1,2,3,4,5,6};
cout << "a=" << a << '\n'
<< "c=" << c << '\n'
<< "d=" << d << '\n'
<< "e=";
for (auto v : e)
cout << v << ' ';
cout << "\nf=";
for (auto v : f)
cout << v << ' ';
a=1
c=3
d=4
e=1 2 3 4 5
f=1 2 3 4 5 6
Using constructors
When a (non-scalar) object is created, its constructor (alias
ctor ) is called. Always. This applies to built-in
classes, e.g., string and vector, and user-defined classes.
A class may have any number of ctors:
string a; // call string::string(), the default ctor
string b(5, 'x'); // call string::string(size_t, char)
string c(b); // call string::string(const string &), the copy ctor
string d("foobar"); // call string::string(const char *)
cout << "a=" << a << '\n'
<< "b=" << b << '\n'
<< "c=" << c << '\n'
<< "d=" << d << '\n';
a=
b=xxxxx
c=xxxxx
d=foobar
Don’t accidentally declare a function
Why doesn’t this work?
string luna(); // 🦡
cout << "luna=" << luna << '\n';
c.cc:1: warning: empty parentheses were disambiguated as a function declaration
c.cc:1: note: remove parentheses to default-initialize a variable
c.cc:2: warning: the address of ‘std::string luna()’ will never be NULL
luna=1
It fails, because string luna();
does not call the default
ctor. Instead, it declares a function named luna
that takes
no arguments and returns a string. If you want to call the default
(no-argument) ctor, omit the parentheses, or use {}
to emphasize
calling the default ctor:
string noche;
string día{};
cout << "noche=" << noche
<< " día=" << día << '\n';
noche= día=
Lifespan
int main() { // 1
string s("hi"); // 2: s is created; its ctor is called.
cout << s << '\n'; // 3
return s.size(); // 4
} // 5: s falls out of scope; its dtor is called.
hi
- Some students confuse ctors & dtors with new and delete.
- All objects get created & destroyed, and hence their ctors & dtors
are called, whether or not they are dynamically allocated via
new & delete.
Lifespan of an object
- Object space is allocated. On the stack, for a local variable,
or on the heap, using new, for dynamic memory. Objects can be in
either place.
- Individual variable ctors are automatically called,
for variables within the object.
- Object’s ctor is called. We already allocated space for the object in
step 1. The ctor code initializes the variables in the object,
if needed beyond their construction in step 2.
- Use the object.
- The object’s life ends. This may be due to it falling out of scope,
if on the stack, or delete being called for a variable on the heap.
The dtor is called, and performs any needed cleanup, often none.
It’s not necessary to zero out the variables.
- Individual variable dtors are automatically called,
for variables within the object.
- Object space is freed. This might be at the end of a
function, when the local variable space is discarded, or it might be
from delete for a variable on the heap.
Language
- The dtor destroys. It does not “destruct”. It certainly does not “delete”.
- Prefer the term destruction for destroying something, in general.
- Only use deletion or deleted when referring to a use of the
delete operator, to get rid of something allocated via new.
- Here,
foo
is destroyed, but it was not deleted;
it was never on the heap.
int main() {
string foo = "bar\n";
cout << foo;
}
bar
Built-in Types
- Built-in types (e.g., int, double, char *) have no ctors.
- However, global and static variables get initialized to zero bits.
- Built-in types have no dtors.
- What would a dtor for a numeric type do? Clear a variable
when it’s about to go away?
- When a pointer falls out of scope, the memory that it points to is
not freed.
- Do not make the mistake of thinking that all pointers point to dynamic memory.
A pointer might very well point to an array on the stack.
Big Picture
- Every class can have:
- a default (zero-argument) ctor
- a copy ctor
- an assignment operator (not a ctor)
- a dtor
- If you don’t write those, the compiler writes default versions.
- However, if you write any ctor, then the compiler will not provide
a default (zero-argument) ctor. It’ll still generate a copy ctor
and assignment operator, though.
- You can delete or replace default versions if you don’t want them.
The compiler also provides a move ctor and a move assignment operator,
but we’re not ready to talk about those.
Default Ctor
class Complex {
public:
Complex() {
real = 0.0;
imag = 0.0;
}
private:
double real, imag;
};
- Its name is the name of the class.
- There is no return type. It isn’t void; there just isn’t one.
- If you don’t provide one, the compiler writes an empty ctor.
Data members get default-constructed anyway.
- However, if you write any ctor, then the compiler will not provide
a default ctor.
Copy ctor
class Complex {
public:
Complex(const Complex &rhs) {
real = rhs.real;
imag = rhs.imag;
}
private:
double real, imag;
};
- Its name is the name of the class.
- There is no return type.
- Argument must be by const reference.
- If you don’t provide one, the compiler writes one
that copies each data member.
- If that’s all you need, just let the compiler do it.
You’ll just forget one of your member variables.
Assignment operator
class Complex {
public:
Complex& operator=(const Complex &rhs) {
real = rhs.real;
imag = rhs.imag;
return *this;
}
private:
double real, imag;
};
- Its name is
operator=
.
- Return a reference to the destination object, so
a=b=c
(assignment chaining) works.
- Argument by const reference.
- If you don’t provide one, the compiler writes one
that assigns each data member.
- If that’s all you need, just let the compiler do it.
You’ll just get it wrong. ☺
- Not a constructor. The object already exists.
Think of renovating an old house, as opposed to building a new house.
class Foo {
public:
Foo() = default;
};
class Bar {
public:
Bar() = delete;
};
If you like the default methods, say so, via =default
, so that
the poor sap reading your code in the future doesn’t have to guess
whether you like the default, or just forgot about it.
Similarly, if you wish to forbid a method, say so, via =delete
.
Same for other default ctors, assignment operator, and dtor.
Member Initialization
- Member variables (data members) often require initialization.
- There are several ways to do it.
- To illustrate, let’s write a class that holds a full name.
- The problem is, people have varying numbers of names:
- First & Last: George Washington
- One name only: Plato
- No names at all: usually involves the police
- We’ll ignore middle names, to keep things simple.
The Old Way
class Name {
string first, last;
public:
Name() { first="John"; last="Doe"; }
Name(string f) { first=f; last="Doe"; }
Name(string f, string l) { first=f; last=l; }
string full() const { return first + " " + last; }
};
Name a, b("Beyoncé"), c("Barack", "Obama");
cout << a.full() << '\n' << b.full() << '\n'
<< c.full() << '\n';
John Doe
Beyoncé Doe
Barack Obama
- Just horrible: repetitious and inefficient.
b.first
gets initialized to ""
, and then assigned
"Beyoncé"
.
- Initialization via
string first;
- Assignment via
first=f;
Member Initialization
Member initialization:
class Name {
string first, last;
public:
Name() : first("John"), last("Doe") { }
Name(string f) : first(f), last("Doe") { }
Name(string f, string l) : first(f), last(l) { }
string full() const { return first + " " + last; }
};
Name a, b("Beyoncé"), c("Barack", "Obama");
cout << a.full() << '\n' << b.full() << '\n'
<< c.full() << '\n';
John Doe
Beyoncé Doe
Barack Obama
- This is better, but we’re still repeating ourselves.
DRY!
- The ctors should invoke each other, via Constructor Delegation.
Incorrect Constructor Delegation
class Name {
string first, last;
public:
Name() { Name("John"); } // 🦡
Name(string f) { Name(f, "Doe"); } // 🦡
Name(string f, string l) : first(f), last(l) { }
string full() const { return first + " " + last; }
};
Name a, b("Beyoncé"), c("Barack", "Obama");
cout << a.full() << '\n' << b.full() << '\n'
<< c.full() << '\n';
Barack Obama
- This was code written by a Java programmer. 😛
- C++ ctor forwarding is different.
- This code just creates unnamed temporary
Name
objects,
and doesn’t do what it should do.
Correct Constructor Delegation
class Name {
string first, last;
public:
Name() : Name("John") { }
Name(string f) : Name(f, "Doe") { }
Name(string f, string l) : first(f), last(l) { }
string full() const { return first + " " + last; }
};
Name a, b("Beyoncé"), c("Barack", "Obama");
cout << a.full() << '\n' << b.full() << '\n'
<< c.full() << '\n';
John Doe
Beyoncé Doe
Barack Obama
Holy smokes—it’s the same syntax as member initialization!
Details
- When should you use member initialization?
- When you have a non-default initial value for your variable.
- When you want to emphasize the default initial value.
- When the data member is expensive to construct, and you
have a non-default initial value to put in it.
- Whenever you can, to initialize a member variable.
- When must you use member initialization?
- When the data member is only initializable, not assignable:
- a const variable
- a reference variable
- an object without a default constructor
- initializing a base class
- Vars constructed in declaration order,
not member initialization list order.
Default member initialization
Sometimes, this is the best technique:
class Name {
string first = "John", last = "Doe";
public:
Name() { }
Name(string f) : first(f) {}
Name(string f, string l) : first(f), last(l) { }
string full() const { return first + " " + last; }
};
Name a, b("Beyoncé"), c("Barack", "Obama");
cout << a.full() << '\n' << b.full() << '\n'
<< c.full() << '\n';
John Doe
Beyoncé Doe
Barack Obama
No assignments here—it’s all initialization.
b.first
does not start as "John"
and then get overwritten
to "Beyoncé"
. And we didn’t repeat ourselves.
Loud.h
% cat ~cs253/Example/Loud.h
// A “Loud” class. It announces whenever its methods are called.
#ifndef LOUD_H_INCLUDED
#define LOUD_H_INCLUDED
#include <iostream>
class Loud {
char c;
void hi(const char *s) const {
std::cout << "Loud::" << s;
if (c) std::cout << " [c='" << c << "']";
std::cout << std::endl; // flush debug output
}
public:
Loud(char ch = '\0') : c(ch) { hi("Loud()"); }
~Loud() { hi("~Loud()"); }
Loud(const Loud &l) : c(l.c) { hi("Loud(const Loud &)"); }
Loud(Loud &&l) : c(l.c) { hi("Loud(Loud &&)"); }
Loud& operator=(const Loud &l) { c=l.c; hi("operator=(const Loud &)"); return *this; }
Loud& operator=(Loud &&l) { c=l.c; hi("operator=(Loud &&)"); return *this; }
Loud& operator=(char ch) { c = ch; hi("operator=(char)"); return *this; }
Loud& operator++() { ++c; hi("operator++()"); return *this; }
Loud operator++(int) { hi("operator++(int)"); const auto save = *this; ++*this; return save; }
Loud operator+(const Loud &l) const { hi("operator+(const Loud &)"); return Loud(c+l.c); }
};
#endif /* LOUD_H_INCLUDED */
Example
#include "Loud.h"
int main() {
Loud a('x');
Loud b(a);
Loud c=a;
Loud d();
c = ++b;
}
c.cc:7: warning: empty parentheses were disambiguated as a function declaration
c.cc:7: note: remove parentheses to default-initialize a variable
c.cc:7: note: or replace parentheses with braces to value-initialize a variable
Loud::Loud() [c='x']
Loud::Loud(const Loud &) [c='x']
Loud::Loud(const Loud &) [c='x']
Loud::operator++() [c='y']
Loud::operator=(const Loud &) [c='y']
Loud::~Loud() [c='y']
Loud::~Loud() [c='y']
Loud::~Loud() [c='x']
Questions & Answers
Why did Loud c=a
call the copy ctor instead of the assignment operator?
- It’s a ctor. It’s creating a
Loud
.
- It can’t be an assignment—assignment applies to an
existing object.
- Yes, the syntax uses a
=
, but it’s a ctor.
- If the ctor is explicit, then only the
()
form can be used.
vector<int> v(3)
, not vector<int> v=3;
.
Why didn’t Loud d()
do anything?
- It’s a function declaration. It declares a function named
d
that takes no arguments, and returns a Loud
.
- If you want to create a
Loud
with no ctor arguments,
just do this: Loud d;
Undesirable Effect
class CheapVec {
public:
CheapVec() : data(nullptr), count(0) { }
CheapVec(int n) : data(new int[n]), count(n) { }
~CheapVec() { delete[] data; }
private:
int *data, count;
};
int main() {
CheapVec a, b(3), c=42; // 🦡
}
How did c=42
compile‽
It called the CheapVec(int n)
ctor.
Or, worse:
class CheapVec {
public:
CheapVec() : data(nullptr), count(0) { }
CheapVec(int n) : data(new int[n]), count(n) { }
~CheapVec() { delete[] data; }
private:
int *data, count;
};
int main() {
CheapVec a;
a = 42; // 🦡
}
The cure
class CheapVec {
public:
CheapVec() : data(nullptr), count(0) { }
explicit CheapVec(int n) : data(new int[n]), count(n) { }
~CheapVec() { delete[] data; }
int size() const { return count; }
private:
int *data, count;
};
int main() {
CheapVec a, b(3), c=42; // 🦡
}
c.cc:12: error: conversion from ‘int’ to non-scalar type ‘CheapVec’
requested
We can also have explicit conversion methods.