Show Lecture.Constructors as a slide show.
CS253 Constructors
Big Picture
- Every class has:
- a default (zero-argument) ctor
- a copy ctor
- an assignment operator (not a ctor)
- If you don’t write those, the compiler writes default versions.
- However, if you write any ctor, then the compiler will not provide
any other ctors.
The compiler also provides a move ctor, a move assignment operator,
and a dtor, 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.
- If you don’t provide one, the compiler writes one that does memberwise
default initialization on data member objects (not scalars).
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 does memberwise copy on the data members.
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 does memberwise copy on the data members.
- Not a constructor. The object already exists.
default
and delete
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 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 involes the police
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é"
.
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.
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.
Delegation
Good:
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.
- 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 init 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?
- Because it’s a ctor. It’s creating a
Loud
.
- Yes, the syntax uses a
=
, but it’s a ctor.
- Note that, if the ctor is
explicit
, then only the ()
form
can be used. vector<int> v(3)
, not vector<int> v=3;
.
- Why did
Loud d()
not do anything?
- Because 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 that final line compile‽
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.