Show Lecture.Constructors as a slide show.
CS253 Constructors
Using constructors
When object is created, its constructor (alias ctor )
is called. Always. This applies for 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 h();
h = "hello";
cout << h << '\n';
c.cc:2: error: assignment of function 'std::__cxx11::string h()'
It fails, because string h();
does not call the default
constructor. Instead, it declares a function named h
that takes no arguments and returns a string. If you want to call
the default (no-argument) constructor, avoid the parentheses:
string b;
b = "bonjour";
cout << b << '\n';
bonjour
Lifespan
int main() { // 1
string s("hi"); // 2: s is created. Its constructor (ctor) is called.
return s.size(); // 3
} // 4: s falls out of scope. Its destructor (dtor) is called.
- 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, broken down
- Get space for the object. On the stack, for a local variable,
or on the heap, using new, for dynamic memory. Objects can be in
either place.
- Call individual data member ctors.
- Call the object’s ctor. 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.
- Call individual data member dtors.
- Free the space for the object. 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”.
- 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 on the heap.
- Here,
foo
is destroyed, but it was not deleted;
it was never on the heap.
int main() {
string foo = "bar\n";
cout << foo;
}
bar
- You can think of built-in types (int, double, char *)
as having ctors & dtors that do nothing.
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 get it wrong. ☺
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 =
, with a space or not.
- Return a reference to the destination object, so
a=b=c
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, 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.
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;
}
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;
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;
- I said this twice because you’ll forget, and spend a really
embarrassing amount of time trying to figure it out.
Trust the voice of experience.
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;
}
free(): double free detected in tcache 2
SIGABRT: Aborted
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.