CS253: Software Development with C++

Fall 2019

Constructors

Show Lecture.Constructors as a slide show.

CS253 Constructors

Big Picture

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

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, 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

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

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

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

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.