Week 12

Setters

In Java, many classes have member functions that start with set. These are called mutators, since they change the state of the object. Mutators should validate data written to the object to enforce the class invariants.

With properly written mutators, the errors described in earlier lessons cannot occur. Consider your Time class. If you were to add setHours() and setMinutes() members to the class, you would have to enforce these restrictions:

  • m_hours must be between 0 and 23 inclusive.
  • m_minutes must be between 0 and 59 inclusive.

Unlike the read() member function, where you could put the stream into a failed state, if these conditions were not met, in a mutator you need to throw an exception like this:

void Time::setHours(int h)
{
    if (h < 0 || h > 23) throw out_of_range("...");
    m_hours = h;
}
Week 12

Getter & Setter Patterns

The pattern of pairing a "getter" along with a "setter" function is common, and you will see it in any major C++ project you work on. Unlike Java, the actual get* and set* name pattern is not as common. Instead, what programmers often do is write a pair of overloaded functions.

Instead of the name getHours() or setHours(), use the name hours() for both of them.

  • The accessor is const and returns a value.
  • The non-const mutator returns a reference, which can be assigned to.
Time Time::hours() const { return m_hours; }  // accessor
Time& Time::hours()                           // mutator
{
    if (h < 0 || h > 23) throw out_of_range("...");
    return m_hours;
}

In general, it is safer to allow clients to read the values of the data members than it is allow them to change those values. As a result, "setters" are far less common than "getters" in class design. Classes with no mutators at all, are called immutable classes.

Week 12

Constructors

Initializing object data is the responsibility of the constructor, which always has the same name as the class and which never has a return type.

class Time
{
public:
    Time();     // default constructor
    ...
};

A constructor is a member function which initializes an object into a well-formed state before clients start manipulating it. When C++ creates an object from a class:

  1. It allocates a block of memory large enough to store the data elements.
  2. It passes the address of that block of memory to the constructor function. The address is the this pointer inside the constructor function.

The constructor is called automatically whenever an object is created. If you have a class that defines a constructor, that constructor is guaranteed to execute whenever you create an object of the class type.

Default Constructors

The default constructor is the constructor which takes no arguments and which should initialize all of its data members to an appropriate default value. Alternatively, since C++11, you may provide an initial value when defining the data members, just as in Java.

If you do not provide a constructor, the compiler will "write" one for you. This is called the synthesized default constructor. If you use in-definition initializers, then this is perfect.

Week 12

Member Initialization

In C++, all constructorsmust initialize all primitive types. A C++ constructor does not need to initialize any object members (like string or vector).

This is exactly the opposite from Java, where you must initialize all of the object instance variables, or they are set to null (an invalid object). In Java, all primitive instance variables are automatically initialized to 0 like this:

public class Point
{
    private String name;
    int x, y;
    public Point() {}
}

Point p = new Point(); // x,y->0, name is null (invalid)

In C++, if you fail to initialize a primitive data member, then it assumes whatever random value was in memory; if you don't initialize an object, such as string or vector, its default constructor will automatically run, and it is still a valid object.

class Point
{
public:
    Point() {}
private:
    string name;
    int x, y;
;}

Point p; // x,y->random, name is valid empty string

Of course, if you provide in-definition initializers for your primitive data members, they will automatically be initialized, even if your construct does not explicitly initialize them.

Week 12

Working Constructor

With the Time class we might like to have another, overloaded constructor which takes hours and minutes. This is generally known as the working constructor. Here is the public interface of Time with both of these constructors.

class Time
{
public:
    Time();                  // default
    Time(int h, int m);      // working
    . . .
};

Unfortunately, if you have any explicit constructors, the synthesized one is deleted, so you have to add an explicit default constructor as done here. In C++11, however, you can just add the phrase =default; to the end of the prototype in the class header, and the compiler will retain the synthesized constructor that it normally writes.

The implementation of the constructors goes into the .cpp file along with the other member functions. The job of the constructor is to initialize the data members, so in the Time class, you might have code that looks something like this.

Time::Time() { m_hours = m_minutes = 0;} 
Time::Time(int hours, int minutes)
{
    m_hours = h; 
    m_minutes = m;
}
Week 12

Assignment vs. Initialization

Before we talk about constructors, look at these two statements:

string a = "Bob", b;      // initialization
b = "Bill";               // assignment

  1. Two string objects are created and initialized on line one; a is initialized using the C-String "Bob", and b is initialized to the empty string by running the default constructor.
  2. The string object b is destroyed (its destructor is run), a new string object is initialized with "Bill", and that new string object replaces the string object originally held by b.

The variable b is first initialized, then destroyed, then assigned. This is inefficient.

Assignment in a Constructor

The body of the constructor is executed after the data members have been initialized. You may use assignment to place a new value into these data members. For primitive types, the cost of doing this is negligible, but for object types, such assignments mean that data members are constructed twice—once at initialization and once at assignement. Here's an example. (The implementation is inline to shorten the code.)

class Person
{
public:
  Person(const string& name) { m_name = name;} 
private:
    string m_name;
};

When you write Person p("Fred"), the m_name data member first calls the default constructor to create an empty string object. Then, in the body of the constructor, the default-constructed string is destroyed when assigning name to m_name. This is inefficient, and you want to avoid it.

Week 12

The Initializer List

We can instruct the compiler to initialize the individual data members before the body of the constructor is entered instead. This is called the initializer list:

  • It follows the parameter list and is preceded by a colon (:)
  • It is followed by a list of member names and their initializers.
  • Initialization occurs in the order the members are declared in the class.

In C++98 the initializers are placed in parentheses; in C++11 use either parentheses or braces. You cannot use the assignment operator. Here is the same class using the initializer list. In this case, the name data member is only constructed once:

class Person
{
public:
  Person(const string& name) : m_name(name) { } // empty body
private:
    string m_name;
};