CS253: Software Development with C++

Spring 2023

Constructors

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

Lifespan of an object

  1. 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.
  2. Individual variable ctors are automatically called, for variables within the object.
  3. 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.
  4. Use the object.
  5. 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.
  6. Individual variable dtors are automatically called, for variables within the object.
  7. 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

int main() {
    string foo = "bar\n";
    cout << foo;
}
bar

Built-in Types

Big Picture

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;
};

Copy ctor

class Complex {
  public:
    Complex(const Complex &rhs) {
        real = rhs.real;
        imag = rhs.imag;
    }
  private:
    double real, imag;
};

Assignment operator

class Complex {
  public:
    Complex& operator=(const Complex &rhs) {
        real = rhs.real;
        imag = rhs.imag;
        return *this;
    }
  private:
    double real, imag;
};

default and delete

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

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

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

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

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

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.