9.1. Constructors

All user defined types include a constructor. Even if you do not write one explicitly, the compiler will write one for you. For most of the classes and structs seen so far, we let the compiler generate the default constructors for us.

Operation

Function Signature

default constructor

X()

copy constructor

X(const X&)

copy assignment

operator=(const X&)

move constructor

X(X&&)

move assignment

operator=(X&&)

destructor

~X()

default constructor

Create a new object in 'the default way'. If you do not write one, the compiler will unless you write your own overloaded constructor.

The default constructor can be overloaded like any other function. When you design your types, you may create constructors with one, two, or more arguments as appropriate.

copy constructor

Create a new object by copying an existing object.

copy assignment

Replace the state of an existing object using another object.

move constructor

Added in C++11. Like copy construction, but the existing source object is invalidated. The state is moved from the source object when the new object is constructed.

move assignment

Added in C++11. Like copy assignment, but the existing source object is invalidated. The state is moved from the source object when the object state is transferred.

Move assignment and move constructors are optional. If you do not define them, the compiler will fall back to the copy constructor and copy assignment instead. However, move semantics may make your code more efficient by replacing potentially expensive copy operations with faster alternatives.

destructor

A destructor is a special function that is always called when an object goes out of scope. The purpose of a destructor is to reclaim any memory or perform any other cleanup tasks that need doing before the object is finally destroyed. A few points about destructors:

  • Like constructors, destructors have no return type.

  • Destructors take no arguments, so they never have a parameter list. There is no way to overload a destructor.

  • We don't call this function ourselves, we leave that to a program as it runs.

We will explore destructors more when we explore containers. For now, just remember that a destructor always exists for every class whether you write one or not and it is always called when the class is destroyed.

The compiler will not generate a default constructor if we write any non-default ones. The compiler may still create other default operations.

So while this is fine:

 1struct point {
 2  int x = 0;
 3  int y = 0;
 4};
 5
 6int main () {
 7  point p;        // implicitly defined constructor
 8  [[maybe_unused]]
 9  point q = p;    // implicitly defined copy constructor
10}

If the only constructor available is a default constructor, then we can only initialize our point using the default constructor.

 1struct point {
 2  int x = 0;
 3  int y = 0;
 4};
 5
 6int main () {
 7  // this works, but it's tedious
 8  point p;
 9  p.x = 8;
10  p.y = 13;
11
12  // This syntax is preferred, but won't compile in C++11
13  point e {3,5};
14}

As a general rule, if we want to initialize class members at construction time, then we need to add custom constructors.

 1struct point {
 2  int x;
 3  int y;
 4  point (int x, int y) : x(x), y(y) {}
 5};
 6
 7int main () {
 8  point p {3,5};  // 2 arg constructor OK
 9  point q = p;    // default copy constructor OK
10  point e;        // compile error
11}

Now we can use our 2 argument constructor, but now our old default invocation is broken.

Why?

Because whenever you write a non-default constructor the compiler will no longer write a default constructor for you.

This is fixed by either:

  • Writing our own default implementation

    point ()
      : x(3), y(5)
    {}
    
  • Telling the compiler to write it

    point () = default;
    
  • Telling the compiler to delete it

    point () = delete;
    

    In the = delete case, attempting the use the default constructor is still a compile error, but the error is more explicit: you can't use the default constructor because it was deleted. The compiler will provide a clear error message saying so.

Important

If you write a non-default constructor, then you should always write your own default constructor, or explicitly instruct the compiler to make it for you, or delete it.

In general, take care deleting the default constructor. Delete it only when you are sure objects of the class will never need to be default constructed or that there is simply no sensible set of defaults for the class.

9.1.1. Initialization syntax

Some programmers coming to C++ from other OO languages sometimes feel as if they have to initialize objects like this:

point p();

Even though all semester, you been writing:

std::string s;
std::vector<int> v;
std::random_device r;

When it comes to user defined types, sometimes it feels 'incomplete' if you don't include the (). Usually, those parentheses create more problems than they resolve. This is because of an inherent ambiguity in the C++ language. Although it seems obvious to us the statement point p(); is a call to the default constructor and the results should be a new variable p, the compiler interprets it differently.

The basic rule is:

If it looks like a function call, it's a function call.

This means that in the code above, the compiler instead looks for:

  • a function named p

  • that takes no arguments

  • and returns an object of type point

Since in this case, there is no such function, it returns an error. Some compilers, like clang, will try to tell you:

point.cpp:7:12: warning: empty parentheses interpreted as a function declaration [-Wvexing-parse]
    point p();
           ^~
point.cpp:7:12: note: replace parentheses with an initializer to declare a variable
    point p();
           ^~

C++11 Feature

C++ resolves this ambiguity in C++11 using the uniform initializer syntax. You can use curly braces: {} instead of parentheses to initialize objects. Braces are an extension of the initializer list syntax for containers and can be used even for default constructed objects.

string s{};
vector<int> v{};
point p{};
string s{"hello, world!"};
vector<int> v{1,2,3,4,5};

While the above works every time, omitting the braces entirely when not needed is preferred:

string s;
vector<int> v;
point p;

Initializer syntax works within constructors as well.

vector<point> redundant {
   point {2,3},
   point {3,5},
   point {8,13},
   point {21,34}
};

// The compiler can deduce the type in the container,
// so we don't have to repeat the type every time
vector<point> points {
   {2,3}, {3,5}, {8,13}, {21,34}
};

Recall that for containers, there is a difference between vector<int>(5) and vector<int>{5}. What's the difference?

Show

The first version creates a vector of size 5 with no initialized values.

The second version creates a vector of size 1 with a single value equal to 5.

9.1.2. Overloaded constructors

The same guidelines that apply to writing good functions apply to writing good overloaded constructors. A good class is built around good functions. Just as with regular functions, avoid confusing parameter lists. Consider the following:

date (int, int, int);

It's seems likely that the three parameters represent the year, month, and day, but without reading the code, there is no way to know what order.

// is this correct?
date d = {1776, 7, 4};

// or this?
date d = {4, 1776, 7};

Even if we read the code and learn the order, it is still probable that we will forget the order and transpose a month and day at some point.

Instead of resigning ourselves to hoping we remember or having debugging problems at runtime, simply defining appropriate types improves clarity and utility:

class year {
  int y;
  public:
    year() = default;
    year(int value) : y {value} {}

    int year() { return y;}
};
enum class month {
  jan=1, feb, mar, apr, may, jun, jul, aug, sep, oct, nov, dec
};

// now a new date can be constructed like:
date d = {year{1776}, month::jul, 4};

This version is easier for programmers to remember and any errors are compile errors instead of runtime errors.

9.1.3. Telescoping constructors

The original date class suffered from a common design problem: too many parameters of the same type. A closely related problem is how to provide flexibility when constructing new objects. A common solution is to provide constructors with different numbers of arguments:

struct date {
   date();                          // make a default date
   date(year y);                    // use a default month and day
   date(year y, month m);           // use a default day
   date(year y, month m, int day);  // specify the entire date
};

What about the possibility of specifying month and day? How many different constructors should be allowed? The number of permutations get unmaintainable even for a relatively few number of parameters.

This is called a telescoping constructor, and is generally considered an anti-pattern. That is, there are better solutions to this problem.

The easiest solution in C++ is to use default values for function parameters. This works best when the default values are different types and there is no need to allow every possible combination of parameters.

struct date {
   date(year y = date::current_year(),
        month m = date::current_month(),
        int day = date::current_day());
};

This solution is still limited by the fact that defaults are still evaluated left to right. A date declaration of the form

date d {15};

won't create a date for the 15th day of the current month and year. In addition, the solution does not work well when all (or most) of the parameters are the same type. Consider this example:

class nutrition_facts {
  private:
    // variables in need of initialization to make valid object
    const double serving_size_;  // mL
    const int servings_;         // per container
    const double calories_;      // Kcal
    const double fat_;           // g
    const double sodium_;        // mg
    const double carbs_;         // g

  public:
    // How about this as a solution?
   nutrition_facts(double, int, double, double, double, double);
};

Is the proper order calories, fat, carbs, or fat, calories, carbs, or something else? Even if we give these parameters meaningful names, there is no runtime enforcement. It's easy to make a mistake when too many parameters are the same type.

When confronted with many optional parameters, a builder is an effective alternative. Basic ideas:

  • Use constructor parameters to accept mandatory parameters.

  • Use a helper class (builder) to default initialize optional parameters.

  • A builder::build() function creates a nutrition_facts object from a builder.

  • The builder makes the class it helps a friend.

    This is used only avoid creating builder accessor functions.

  • A conversion constructor is used to copy builder state into the enclosing class.

A nutrition facts class
#include <iostream>

class nutrition_facts {
  private:
    // variables in need of initialization to make valid object
    const double serving_size_;  // mL
    const int servings_;         // per container
    const double calories_;      // Kcal
    const double fat_;           // g
    const double sodium_;        // mg
    const double carbs_;         // g

  public:
    // Only one simple constructor for mandatory parameters.
    // The remaining members are handled by builder
    nutrition_facts( const double serving_size, const int servings)
      : serving_size_{serving_size}, servings_{servings},
      calories_{0}, fat_{0}, sodium_{0}, carbs_{0}
    {}

    // use this class to construct nutrition_facts
    class builder {
      private:
        friend nutrition_facts;
        double serving_size_ = 15;  // mL
        int servings_ = 10;         // per container
        double calories_ = 0;       // Kcal
        double fat_ = 0;            // g
        double sodium_ = 0;         // mg
        double carbs_ = 0;          // g

      public:
        builder() = default;

        // create a nutrition_facts object from a builder
        nutrition_facts build() {
          return nutrition_facts (*this);
        }

        builder& serving_size(const double size) {
          serving_size_ = size;
          return *this;
        }
        builder& servings(const int s) {
          servings_ = s;
          return *this;
        }
        builder& calories(const double c) {
          calories_ = c;
          return *this;
        }
        builder& fat(const double f) {
          fat_ = f;
          return *this;
        }
        builder& sodium(const double s) {
          sodium_ = s;
          return *this;
        }
        builder& carbohydrates(const double c) {
          carbs_ = c;
          return *this;
        }

    };

    explicit nutrition_facts(const builder& builder)
      : serving_size_{builder.serving_size_},
      servings_{builder.servings_},
      calories_{builder.calories_},
      fat_{builder.fat_},
      sodium_{builder.sodium_},
      carbs_{builder.carbs_}
    {}

    double serving_size() const { return serving_size_; }
    int servings() const { return servings_; }
    double calories() const { return calories_; }
    double fat() const { return fat_; }
    double sodium() const { return sodium_; }
    double carbohydrates() const { return carbs_; }
};


std::ostream& operator<<(std::ostream& os, const nutrition_facts& rhs) {
  return os << "Serving size: " << rhs.serving_size()
            << "\tServings: " << rhs.servings()
            << "\n\tCal: " << rhs.calories()
            << "\n\tFat: " << rhs.fat()
            << "\n\tSodium: " << rhs.sodium()
            << "\n\tCarbs: " << rhs.carbohydrates();
}

When complete, the classes can be used like this:

 1#include <iostream>
 2
 3int main() {
 4  // make facts without any optional parts
 5  auto cake = nutrition_facts{75, 8};
 6
 7  // create a builder
 8  nutrition_facts::builder b;
 9
10  // change the state
11  b.serving_size(28.4).servings(1);
12  b.fat(10).sodium(2).calories(150).carbohydrates(15);
13
14  // create a set of nutrition facts using the builder
15  auto chips = b.build();
16
17  // create nutrition facts without creating a (named)
18  // temporary builder object
19  auto soda = nutrition_facts::builder()
20              .serving_size(368).servings(1)
21              .carbohydrates(40).calories(150).sodium(15).build();
22
23  std::cout << "Cake:\t" << cake << '\n';
24  std::cout << "Chips:\t" << chips << '\n';
25  std::cout << "Soda:\t" << soda << '\n';
26
27  return 0;
28}

While not the most idiomatic C++ solution, it is something we can create and use with only the knowledge of classes we have so far.


More to Explore