7.2. Constructors

When we allocate storage for a built-in type, we use the name of the type and a name for the variable:

int x;

As we saw in the previous section, the syntax for user-defined types is the same:

Talk say;

User-defined types use special functions to construct an object using the class definition. We call these special functions class constructors. Constructors are very similar to regular free functions, but there are a few special rules:

All classes must have at least 1 constructor. If you do not write one, then compiler will try to create it automatically. The Talk class works because it used an automatically defined default constructor generated by the compiler.

#include <cstdio>

struct Talk {
  void hello() {
    std::puts("Hello, world!");
  }
};

int main() {
  Talk say;     // Create an object from a class
  say.hello();  // Call a function in the object
}

Although we did not need to create a constructor for the Talk class, we could have:

This constructor doesn’t change how our code functions in any way. The compiler would have generated the code Talk() {}; for us.

#include <cstdio>

struct Talk {
  Talk() {}        // default constructor

  void hello() {
    std::puts("Hello, world!");
  }
};

int main() {
  Talk say;     // Create an object from a class
  say.hello();  // Call a function in the object
}

In C++11, we can instruct the compiler to create the default constructor for us with

Talk() = default;

7.2.1. Object life cycle

In C++, objects get created, used, and destroyed. Constructors, assignment functions, and destructors control the life cycle of objects: creation, copy, move, and destruction.

Like the default constructor, C++ defines default operations for other parts of an object life cycle. The default operations are a set of related operations that together implement the life cycle semantics of an object.

The list of default operations since C++11 is:

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()

We will be discussing these operations more over the next several chapters.

Let’s improve our Talk class by making it possible to say more than one thing and to set the default text in a constructor:

#include <cstdio>
#include <string>

using std::string;

struct Talk {
  string text_;               // a variable to store what we want to say

  Talk()                      // a new default constructor
    : text_ ("Hello, world!")
  { }

  Talk(const string& value)    // a one argument constructor
    : text_ (value)
  { }

  void text() {
    std::puts(text_.c_str());
  }
};

int main() {
  Talk say;     // Create the default object
  say.text();

  say.text_ = "Something else";
  say.text();

  // Create a non-default object
  Talk talk("The 80's were a long time ago.");
  talk.text();
}

Note that we also changed the name of our function from hello to text. Our old function name is no longer very appropriate since we can say more things than just Hello, world!.

Since C++11, this syntax for constructors:

Talk(const string& value)
  : text_ (value)
{ }

is preferred.

This is called initializer list syntax. The general format is:

ClassName(arguments)
  : class_member1 {expression1},
    class_member2 {expression2},
    class_memberN {expressionN} . . .
{ }

Initializer list expressions can be surrounded by ( ) or { }.

Prior to C++11, standard function syntax was used. It is still allowed, but initializer list syntax is preferred.

ClassName(arguments) {
  class_member1 = expression1;
  class_member2 = expression2;
  class_memberN = expressionN;
}

In C++11, it is also permissible to initialize class members with constants directly in the class when declared. In-class initialization is preferred because it makes it explicit that the same value is expected to be used in all constructors, avoids repetition, and avoids maintenance problems. It leads to the shortest and most efficient code. Consider the following:

class BadInit {
    int i;
    string s;
    int j;
  public:
    BadInit() :i{666}, s{"nothing"} { }  // j is uninitialized
    BadInit(int value) :i{value} {}      // s is "" and j is uninitialized
    // ...
};

How would a maintainer know whether j was deliberately uninitialized (probably a poor idea anyway) and whether it was intentional to give s the default value "" in one case and nothing in another? This is almost always a bug. Forgetting to initialize a member often happens when a new member is added to an existing class.

All these problems are easily fixed with in-class initializers:

class OkInit {
    int i {666};
    string s {"ok"};
    int j {0};
  public:
    OkInit() = default;              // all members initialized to default values
    OkInit(int value) :i{value} {}   // s and j initialized to their defaults
    // ...
};

A common error is to confuse constructors with other functions.

7.2.2. Class invariants

A struct is acceptable to define a type as long as every struct member may be assigned any value at any time. If this is not true for your type, then we say that your type has invariants. Class invariants are guarantees made by your type. Invariants represent things that must hold true for your class to be valid.

Let say we need to prevent Talk from allowing zero length strings in the member text_. Currently, since everything is public, but it is easily fixed:

#include <iostream>
#include <string>

using std::string;

class Talk {
  public: 

    Talk() : text_ ("Hello, world!") { }

    Talk(const string& value) 
      : text_ {value.empty()? "default text": value} 
    { }

    string text() {
      return text_;
    }
    void text(string value) {
      if (value.empty()) return;
      text_ = value;
    }

  private:
    string text_;
};

int main() {
  Talk say;
  std::cout << say.text() << '\n';

  say.text("Something else");
  std::cout << say.text() << '\n';
  say.text("");
  std::cout << say.text() << '\n';
}

Without running this program, can you predict it’s output?

Our class now enforces its invariants that it will never allow the Talk text to be empty (we’re a talkative Talk class).

We made several changes:

  • The member text_ is now private.

  • A function void text(string value) was added. This was needed because we made text_ private. Without this function, the only way to set the text was in the constructor.

  • Added an additional check to our one argument constructor to enforce the “can’t be empty” invariant.

Try This!!

Modify the last example so that it accepts an additional class member variable to repeat what Talk says more than once.


More to Explore

You have attempted of activities on this page