Functions in C++

Steven J. Zeil

Last modified: Oct 26, 2023
Contents:

Once we have a grasp of what data we want to use, our attention passes to the code that manipulates that data. That code will be contained within functions, and here we will survey some of the variations you can encounter in putting functions together.

1 Standalone Functions

A function is “standalone” if it is not declared as a member of a class or a struct.

The canonical example of a standalone function is the one that starts off the execution of every program:

int main (int argc, char** argv)
{
  ⋮
  return 0;
}

But there are certainly others.

double sqrt(double x);  // Compute the square root of x
double abs(double z);   // Compute the absolute value of z

The list of parameters in a function’s declaration are called its formal parameters.

To call a standalone function, you simply write its name in an expression, followed by a parenthesized list of expressions that give the actual data you want used in the computation, the actual parameters.

double num = abs(3.0);     // formal parameter z gets an actual parameter of 3.0
cout << sqrt(num) << endl; // formal parameter x gets an actual parameter of num

Suppose that I was writing code for part of a bookstore’s inventory system. I might have the idea of a class Book. Now, in an older programming language that had only standalone functions, I might expect to write code like this:

for (Book b: inventory)
{
  Book sequel = b;
  setTitle(sequel, "The Further Adventures of " + getTitle(b));
  addToInventory(sequel);
}

That would be based upon standalone functions for getTitle() and setTitle(), which might look like this:

struct Book {
    std::string title;
     ⋮
};

std::string getTitle(const Book& book)
{
  return book.title;
}

void setTitle(Book& book, const std::String& newTitle)
{
  book.title = newTitle;
}

In current practice, programs tend to have relatively few standalone functions. The bulk of the functions in most programs are member functions.

2 Member Functions

 

A member function is a function that is declared within a class or struct.

Returning to our Book example, a more modern style would make getTitle and setTitle into member functions:

I would likely write something like this in book.h:

class Book {
    std::string title;
     ⋮
public:
     ⋮
    std::string getTitle() const;
    void setTitle(const std::String& newTitle);
};

And then in book.cpp, I would put:

std::string Book::getTitle() const
{
  return title;
}

void Book::setTitle(const std::String& newTitle)
{
  title = newTitle;
}

Member functions are called by putting the object that they are applied to on the left, followed by a ‘.’ (or -> if we are using pointers), then the function name and remaining parameters.

for (Book b: inventory)
{
  Book sequel = b;
  sequel.setTitle("The Further Adventures of " + b.getTitle());
  addToInventory(sequel);
}

Compare this to the earlier standalone function version:

  setTitle(sequel, "The Further Adventures of " + getTitle(b));

and you can see how member functions simply shift the first parameter of each call outside of the parentheses and to the left. This is considered to be more expressive and suggestive of the function’s being a member of the class, analogous to the data members, but it is, in the end, more a matter of style than of substance.

2.1 This is the Mysterious Missing Parameter

It’s easy enough to see that the calls to the functions

setTitle(aBook, "War and Peace II"); // standalone
aBook.setTitle("War and Peace II"); // member

actually provide the same number of parameters, but how do we reconcile the following declarations?

void setTitle (Book& b, const std::string& newTitle); // standalone 

class Book
{
    ⋮
    void setTitle (const std::string& newTitle); // member
    ⋮
};

Clearly the standalone version is declared with two parameters, but the member function with only one. How does the member function know which book to set the title of?

The answer lies in a bit of sleight-of-hand by the C++ compiler. Member functions have an implicit parameter that we never type out as part of the declaration, but that the compiler automatically supplies. This implicit parameter is the first “true” parameter to the function, it’s data type is a pointer to a value of the class type, and its name is “this”.

class Book
{
    ⋮
    void setTitle (const std::string& newTitle); // This is what we type
    /*
    void setTitle (Book* this, const std::string& newTitle); // This is what the compiler does
    */
    ⋮
};

When we call a member function, the compiler does a similar bit of legerdemain:

aBook.setTitle ("The Adventures of Sherlock Holmes");

is actually translated as

Book::setTitle (&aBook, "The Adventures of Sherlock Holmes");

When we provide the function body for the member function setTitle, we can do so like this:

void Book::setTitle (const std::string& newTitle)
// really:  void Book::setTitle (Book* this, const std::string& newTitle)
{
    this->title = newTitle;
}

However, these is one final magic trick available from the compiler. Within the body of a member function, we can almost always omit the phrase this-> wherever it appears. As a consequence, the vast majority of the member function code you will ever read or write will never mention this:

void Book::setTitle (const std::string& newTitle)
// really:  void Book::setTitle (Book* this, const std::string& newTitle)
{
    title = newTitle;
}

You will occasionally find yourself in situations where you need to use this explicitly. One is when you need to manipulate the entire object rather than just its members. For example:

class Book
{
  ⋮
  void putIntoDisplayWindow (BookStore& store);
  ⋮
};
    ⋮

void Book::putIntoDisplayWindow (BookStore& store)
{
  store.window.add(*this);   
}

Another situation where you need to use this is when failing to do so would cause ambiguity. For example, suppose that we had declared our setTitle member function like this:

class Book
{
    ⋮
    void setTitle (const std::string& title);
    ⋮
private:
    std::string title;
};

so that we would be tempted to implement it like this:

void Book::setTitle (const std::string& title)
{
  title = title;
}

The highlighted assignment does not do what we want because of the ambiguity of what “title” refers to. We want the first “title” to be the data member and the second “title” to be the function parameter. But, in fact, the compile is going to assume that both refer to the function parameter.

There are three ways to fix this:

  1. Rename the function parameter to, e.g., newTitle and write the assignment as title = newTitle;
  2. Rename the data member to, e.g., _title and write the assignment as _title = title;
  3. Don’t rename anything, but rewrite the assignment as this->title = title;

Within the C++ programming community, you will encounter each of these three options. It’s something of a matter of personal style. Many programmers will give all of their data members a prefix such as an underscore (_title) or “the” (theTitle). Other programmers will use the “the” prefix for their function parameter names. A smaller number of programmers will not worry about it, but simply use an explicit this-> when necessary. (The majority of programmers will argue that using the same name for a data member and a function parameter is just asking for trouble, however, because it’s all too easy to forget a this-> when you really need it).

2.2 Const Member Functions

In general, some of the parameters to a function may be inputs that the function body will look at but not change, and other parameters are outputs that the function body will change. There are also in-out parameters that the function body look both at the initial value passed in, but also changes that value as output.

You should be used to the idea that you can look at a function’s parameter list and tell whether a parameter is pure input or some form of output (either output only or an in-out combo).

int doSomething (Book b1, Book& b2, const Book& b3);

b3 is an input (only) because, as we reviewed in a previous lesson, const references can be used to examine data but not to change the data. Remember also that the compiler enforces this limitation on const references. If we had a function

void addToCatalog(Catalog& catalog, const Book book)
{
  ⋮
  book.setTitle("My Autobiography");  // Error!
  ⋮
}

The setTitle call would be flagged as a compilation error because addToCatalog says in its header that book is a const reference – the designer of addToCatalog made a promise not to change book, and the compiler is going to enforce that promise.

But we would expect that this code should be perfectly legal:

void addToCatalog(Catalog& catalog, const Book book)
{
  catalog.addTitle (book.getTitle()); // Should be OK!
}

How does the compiler know that setTitle changes the book, but that getTitle does not?

If the compiler were not playing funny now-you-see-it-now-you-don’t games with the this parameter, we would be able to make a promise that getTitle would leave the book alone. We would write

void Book::setTitle (Book* this, const std::string& newTitle);
std::string Book::getTitle(const Book* this);

By making this a const pointer, we would be promising to not change the value of the book, and the compiler would enforce that promise, and would know that it could count upon that promise in addToCatalog when we call getTitle.

But we know that the compiler is going to wave its magic wand and make those this declarations disappear. So how do we indicate that one of those disappearing this declarations should be const and the other should not?

Apparently the designers of the C++ language thought long and hard about this, and finally decided to pick an arbitrary place to put the const – right at the end of the declaration. So what we actually write is

class Book
{
    ⋮
    void setTitle (const std::string& newTitle);
    std::string getTitle() const;
};

and

void Book::setTitle (const std::string& newTitle)
{
  title = newTitle;
}

std::string Book::getTitle() const
{
  return title;
}

2.2.1 Const Correctness

A good C++ class designer will be very careful to designate which member functions are const or not, and which function parameters are const or not.

A class declaration is const correct if

  1. Each member function has been marked as const if and only if it does not need to change the object it is applied to.
  2. Each function parameter is passed as a non-const reference if it serves as an output (or in-out) of the function, passed by copy or as a const reference if it is input only.

Achieving const correctness takes a bit of care for newer C++ programmers, but pretty quickly becomes second nature. Failure to achieve const correctness will generally result in a flurry of annoying compilation errors, including the rather cryptic “discards qualifier” message.

2.3 Constructors

Constructors are special functions that are used to create & initialize a new value of a class type. They are still functions, so we can declare them to have parameters, allowing us to pass in any data we need to properly initialize a new object.

For example, I might decide that every Book should be initialized with a title and its year of publication:

class Book
{
 public:
    Book (const std::string& theTitle, int year);
    void setTitle (const std::string& theTitle);
    std::string getTitle() const;
    void setYearOfPublication(int theYear);
    int getYearOfPublication() const;
       ⋮
 private:
    std::string title;
    int year; 
};

And we could implement that as

Book::Book (const std::string& theTitle, int theYear)
{
  title = theTitle;
  year = theYear;
}

2.3.1 Constructors can be used to declare variables

The most common use for a constructor is in declaring new variables, e.g.,

Book myBook ("My Autobiography", 2022);

Storage space for myBook is reserved, and then the constructor is called to initialize that storage.


Constructors with one parameter

If a constructor takes exactly one parameter, C++ actually allows the declaration to be written two different ways.

For example, if we had

class Counter {
 public:
    Counter(int initialValue); 
      ⋮
};

then we could create a new counter like this

    Counter ctr(100);  // the "normal" syntax for calling a constructor

or like this:

    Counter ctr = 100;  // the "special" syntax for a constructor with 1 parameter

They both mean exactly the same thing. The ‘=’ is not an assignment – it’s an alternative syntax to the use of parentheses.

2.3.2 Constructors are still functions

Constructors can also be called just like any function. In that case, they are used to initialize a new value of the class type and they return that newly constructed value to their caller.

addToCatalog (catalog, Book("My Autobiography", 2022));

is logically equivalent to

Book temp ("My Autobiography", 2022);
addToCatalog (catalog, temp);

2.4 Some Constructors are Special

There are a few patterns for constructors that are so common in C++ that they have been given names.

2.4.1 Default Constructors

A default constructor is a constructor that can be called with no parameters.

If you were to write, for example,

std::string s1;

you are invoking the default constructor for std::string (which initializes the variable to the empty string).

Most often, default constructors are declared with no parameters:

class Shipment
{
 public:
    Shipment();  // default constructor
       ⋮
};

It can be called like this:

Shipment aShipment;

A constructor can also qualify as a default constructor if it provides default values for all of its parameters:

class Book
{
 public:
    Book (const std::string& theTitle = std::string(), int year = 2000);
       ⋮
 private:
    std::string title;
    int year; 
};

The three declarations below are all equivalent:

Book b;
Book b(std::string());
Book b(std::string(), 2000);

Default Constructors and arrays

Default constructors are important because, when we create an array of class values, e.g.,

Shipment deliveries[100];

the default constructor for that type is used to initialize every element of the new array.

If a class does not have a default constructor, then you cannot create arrays of that class.

2.4.2 Copy Constructors

Another special constructor is one that takes a single parameter, a const reference to the class being constructed, e.g.,

class Book {
 public:
    Book(); // default constructor
    Book (const Book&); // copy constructor
       ⋮
};

This is called the copy constructor because it is used to create an object that is a copy of another.

What makes copy constructors special is not how they work, but, rather, how often they get used.


Where are Copy Constructors Used?

The copy constructor gets used in 5 situations:

  1. When you declare a new object as a copy of an old one:

    Book book2 (book1);
    

    or

    Book book2 = book1;
    
  2. When a function call passes a parameter “by copy” (i.e., the formal parameter does not have a &):

    void foo (Book b, int k);
      ⋮
    
    Book text361 (...);
    foo (text361, 0);   // foo actually gets a copy of text361
    
  3. When a function returns an object:

    Book foo (int k);
    {
      Book b;
      ⋮
      return b; // a copy of b is placed in the caller's memory area
    }
    
  4. When data members are initialized in a constructor’s initialization list from a single parameter of the same type:

    Author::Author (std::string theName, 
                    Address theAddress, long id)
      : name(theName), 
        address(theAddress),
        identifier(id)
    {
    }
    

    We’ll cover initialization lists in the next section.

  5. When an object is a data member of another class for which the compiler has generated its own copy constructor.

    We’ll cover this use of copy constructors in a later lesson.

2.4.3 Initialization Lists

There are actually two stages to the execution of a constructor:

  1. The data members of the class are initialized.
  2. The function body of the constructor is run.

This code is actually a bit inefficient:

Book::Book (const std::string& theTitle, int theYear)
{
  title = theTitle;
  year = theYear;
}

The reason I say that is because those two assignment statements in the function body are not initializing the data. They are actually replacing the values of the already initialized data members.

Only after the data members have all been initialized does the code inside the { } start to execute.

It would be more efficient to start off initializing the data members to the values we want, so that we don’t need to reassign them a few microseconds later. This can be done by adding an initialization list to the implementation fo the constructor.

An initialization list is a comma-separated list of the data members of the class, each followed, in parentheses, by the parameters that we would supply to a constructor to initialize that data member. The whole list appears between the function header and the body, with a colon (‘:’) right in front of it:

Book::Book (const std::string& theTitle, int theYear)
: title(theTitle), year(theYear)
{
}

In simple classes like Book, it may not matter a whole lot whether you use initialization lists or wait until the function body to reassign the proper values. There are, however, situations in which a data member can only be initialized in the initialization list:

2.5 Destructors

The flip-side of initializing new objects is cleaning up when old objects are going away. Just as C++ provides special functions, constructors, for handling initialization, it also provides special functions, destructors, for handling clean-up.

A destructor for the class Foo is a function of the form

~Foo();

You must never call a destructor explicitly.

Instead the compiler generates a call to an object’s destructor when

The most common use of destructors is to prevent memory leaks when classes have data members that are pointers to objects allocated on the heap.

For example, suppose that we wanted to track the authors of a book:

class Book {
 public:
     Book(const std::string& theTitle = std::string());
     ~Book();
        ⋮
     void addAuthor (const Author& author);
     int numAuthors() const;
     Author getAuthor(int index) const;
        ⋮
 private:
     std::string title;
     int nAuthors;
     static const int maxAuthors = 12;
     Author* array;
};

Then it is likely that the array of authors would be allocated on the heap by the constructor:

Book::Book(const std::string& theTitle)
: title(theTitle), nAuthors(0), authors(new Author[maxAuthors])
{
}

in which case, the appropriate action for the destructor would be to free up that memory

Book::~Book()
{
    delete [] authors;
}

Without that destructor, every book object would, when constructed, allocate an array on the heap, and that block of memory would remain, unusable, after each book object had disappeared.

3 Inline functions

Many of the member functions we have given as examples are simple enough that we might consider an alternate approach, declaring them as inline functions.

We’ve seen in our discussion of automatic storage that there’s a lot of bookkeeping that goes on behind the scenes when we call a function and when we return from them.

The amount of time spent on this overhead is not really all that large. If the function body contains several statements in any kind of loop, then the overhead is probably a negligible fraction of the total time spent on the call. But if the function body is only a line or two and does not involve a significant amount of computation, then the time spent on that overhead can be, by comparison, quite large.

Many ADTs have member functions that are only one or two lines long, and often trivial lines at that. In our Book class, for example, we might feel that functions like getTitle and setTitle meet that description.

For these functions, the overhead associated with each call may exceed the time required to do the function body itself. Furthermore, because these functions are often the primary means of accessing the ADT’s contents, sometimes these functions get called thousands of times or more inside the application’s loops, causing the total amount of time spent on the overhead of making the function call to become significant.

For these kinds of trivial functions, C++ offers the option of declaring them as inline.

An inline function can be written one of two ways, both of which are illustrated here:

class Book {
 public:
     Book(const std::string& theTitle = std::string());
     ~Book();
        ⋮
     void setTitle (const std::string& newTitle) {title = newTitle;}
     std::string getTitle () const;
     
     void addAuthor (const Author& author);
     int numAuthors() const;
     Author getAuthor(int index) const;
        ⋮
 private:
     std::string title;
     int nAuthors;
     static const int maxAuthors = 12;
     Author* authors; // pointer to array
};

inline std::string Book::getTitle()
{
  return title;
}
  1. It can be written inside the class declaration.
  2. We can place the reserved word inline in front of the function definition written in its usual place outside the class declaration.

Either way, we then remove the function body from the .cpp file.

When we make a call to an inline function, the compiler simply replaces the call by a compiled copy of the function body (with some appropriate renaming of variables to avoid conflicts).

So, if we have

inline int foo(int a, int b)
{
  return a+b-1;
}

and we later make a call

  x = foo(y,z+1);

This would be compiled into a code sequence along the lines of

evaluate z+1, storing result in tempB 
evaluate y + tempB - 1, storing result in x

Most of the overhead of making a function call has been eliminated.

Inline functions can reduce the run time of a program by removing unnecessary function calls, but, used unwisely, may also cause the size of the program to explode. Consequently, they should be used only by frequently-called functions with bodies that take only 1 or 2 lines of code. For larger functions, the times savings would be negligible (as a fraction of the total time) while the memory penalty is more severe, and for infrequently used functions, who cares?

Inlining is only a recommendation from the programmer to the compiler. The compiler may ignore an inline declaration and continue treating it as a conventional function if it prefers. In particular, inlining of functions with recursive calls is impossible, as is inlining of most virtual function calls (don’t worry if you don’t know what those are). Many compilers will refuse to inline any function whose body contains a loop. Others may have their own peculiar limitations.

3.1 Static Member Functions

In the prior less, we saw that the modifier static could be applied to data members of a class to indicate that the data belonged, not to each object of the class, but to the class as a collection, one instance of that data member being shared by all the values of that class.

We can do something similar with functions declared inside a class. static applied to a member function means that it is not a a member of the objects but of the class as an entirety. (It’s questionable whether these should really be called “member” functions, but the term “static member function” is pretty well established.)

Static member functions can only use other static members, functions or data, of that class, because a static function does not receive a specific object to work with.

For example, suppose that we wanted to keep track of how many different Book objects had been created. We could use a static data member to hold a counter, increment that counter every time a book is constructed, and provide a static function to retrieve the value of that counter.

// in book.h
class Book
{
  public:
     Book();
     ⋮
     static int booksCreated();
  private:
     std::string title;
     static int counter;
};


// in book.cpp
int Book::counter = 0;  // initialize the counter to zero

Book::Book()
{
  ++counter;
}


int Book::booksCreated()
{
  return counter;
}

Technically speaking, constructors are static member functions, but they have so many exceptions and special behaviors of their own that they barely play by the same rules as normal static member functions.

4 Overloading

Two functions are said to be overloaded if they have the same name but differ in the number and/or data types of the parameters that they take.

4.1 Ordinary Overloading

If we write

void foo(int i) 
{
  cout << "my parameter is an integer" << endl;
}

void foo(std::string s) 
{
  cout << "my parameter is a string" << endl;
}

void foo(int i, std:string s)
{
  cout << "I have two parameters" << endl;
}


⋮

foo(0);
foo("hello");
foo(42, "42");

We would expect to see the output:

my parameter is an integer
my parameter is a string
I have two parameters

The compiler looks at the data types of the actual parameters in the function calls and compares them to the data types of the formal parameters in the function declarations. It chooses whichever function actually matches the parameter types supplied.

Overloading is very common in C++. We actually had an example of it earlier:

class Book {
 public:
    Book(); // default constructor
    Book (const Book&); // copy constructor
       ⋮
};

Both functions have the same name (“Book”), but one takes no parameters and the other takes a single parameter of type const Book&, so the compiler can easily determine that

Book b1;

calls the default constructor and

Book b2 (b1);

calls the copy constructor.

4.2 Operator Overloading

One of the truly delightful, or demonic, depending on your point of view, aspects of C++ is that almost every operator in the language can be declared in our own functions. In most programming languages, we can write things like “ x+y ” or “ x<y ” only if x and y are integers, floating point numbers, or some other pre-defined type in the language. In C++, we can add these operators to any class we design, if we feel that they are appropriate.

The basic idea is really very simple.

Almost of the things we use as operators, including

+ - * / | & < > <= >= = == != ++ -- -> += -= *= /=

can be overloaded.

This is also true of some things you probably don’t consider as operators (the [ ] used in array indexing and the ( ) used in function calls). There are only a few things that look like operators but cannot be overloaded: ‘.’ and ‘::’.

All of those operator symbols are actually “syntactic sugar” for a function whose name is formed by appending the operator itself to the word “operator” .


For example, if you write a + b*(-c), that’s actually just a shorthand for

operator+(a, operator*(b, operator-(c)))

and if you write

testValue = (x <= y);

that’s actually a shorthand for

operator=(testValue, operator<=(x, y);

Now, this shorthand is so much easier to deal with that there’s seldom any good reason to actually write out the long form of these calls. But knowing the shorthand means that we can now declare new operators in the same way we declare new functions.

4.3 Example: Date Operations

Suppose that we were working with a class to manipulate calendar dates. We could start with a class like this:

class Date {
 private:
   int daysSinceEpoch;  
 public:
   Date ();
   Date (int day, int month, int year);
   
   int getDay() const;  
   void setDay (int day);
   
   int getMonth() const;
   void setMonth(int month);
   
   int getYear() const;
   void setYear();
   
   Date dateSomeDaysAway(int numDaysAway) const;
   int numberOfDaysApart(const Date& from) const;
};

This would let us write application code like:

Date rescheduleWeeklyMeeting(const Date& meeting)
{
    Date followUp = meeting.dateSomeDaysAway(7);
    return followUp;
}

or

void warnIfBookIsOverdue (const Date& checkoutDate, const Date& today)
{
    if (today.numberOfDaysApart(checkoutDate) > 21)
    {
        cout << "This book is overdue." << endl;
    }
}

The function bodies for the two operations given as

Date Date::dateSomeDaysAway(int numDaysAway) const
{
    Date result;
    result.daysSinceEpoch = daysSinceEpoch + numDaysAway;
    return result;
}
   
int Date::numberOfDaysApart(const Date& from) const
{
    return daysSinceEpoch - from.daysSinceEpoch;
}

Now, the names of the last two operations are a bit awkward. Perhaps you could think of better ones. But I honestly didn’t worry about it at the time because I knew that I was eventually going to replace these by operators.

class Date {
 private:
   int daysSinceEpoch;  
 public:
   Date ();
   Date (int day, int month, int year);
   
   int getDay() const;  
   void setDay (int day);
   
   int getMonth() const;
   void setMonth(int month);
   
   int getYear() const;
   void setYear();
   
   Date operator+(int numDaysAway) const;
   int operator-(const Date& from) const;
};

All that we have done here is to change the names of the two functions, not their behavior. And that’s all that would change in the C++ class declaration:

I think, however, that this makes for much nicer application code:

Date rescheduleWeeklyMeeting(const Date& meeting)
{
    Date followUp = meeting + 7;
    return followUp;
}

or

void warnIfBookIsOverdue (const Date& checkoutDate, const Date& today)
{
    if (today - checkoutDate > 21)
    {
        cout << "This book is overdue." << endl;
    }
}

For the eventual implementation, again, all that changes are the function names:

Date Date::operator+(int numDaysAway) const
{
    Date result;
    result.daysSinceEpoch = daysSinceEpoch + numDaysAway;
    return result;
}
   
int Date::operator-(const Date& from) const
{
    return daysSinceEpoch - from.daysSinceEpoch;
}

Operator functions seem to cause some students a great deal of anxiety, but they really aren’t all that complicated.

When we declare and implement them, it’s just a change to a function name that starts with “operator”.

It is only when we use them that their special nature comes out, as they provide us with a shortcut to writing out the full function name.


Not every class make sense with operators like “+” or “-”. I have no idea, for example, what it would even mean to add two Books together or to subtract one Book from another.

There are, however, a few operators that would make sense for Book and for almost all ADTs.

The most commonly overloaded operators are:

We’ll talk about the assignment and comparison operators in some detail in later lessons, and we’ll look at the output operator in just a moment.

4.3.1 Standalone vs Member Operator Functions

One thing to watch for when using operator functions is the difference between standalone and member functions.

Let’s revisit the example of adding an integer number of days to a Date. I want to be able to write something like

Date today = getTodaysDate();
Date aWeekFromToday = today + 7;

One way to do that would be to have a standalone function:

Date operator+ (const Date& date, int nDays);

If we had that declaration, then when we write today + 7, the compiler matches our use of + against this declaration and translates it to operator+(today, 7).

What if we wanted to write:

Date today = getTodaysDate();
Date aWeekFromToday = today + 7;
Date alsoAWeekFromToday = 7 + today;

The + in the third line does not match our declared operator+, because the incoming parameters are int and Date rather than Date and int. So this would not compile. But it’s easily fixed. We just overload the operator+ function.

Date operator+ (const Date& date, int nDays);
inline Date operator+ (int nDays, const Date& date) 
{
   return date + nDays;  // call the first operator+ instead
}

But instead of using standalone functions, we can write operators as member functions instead:

class Date {
 private:
   int daysSinceEpoch;  
 public:
   Date ();
       ⋮ 
   Date operator+(int nDays) const;
};

Notice that this function appears to only have one parameter instead of the two that we need. But that’s just because it’s a member function. All (non-static) member functions have the implicit this parameter that does not appear within the parentheses.

If we have that declaration instead of our standalone versions, then the compiler will translate the call

Date today = getTodaysDate();
Date aWeekFromToday = today + 7;

to

Date today = getTodaysDate();
Date aWeekFromToday = today.operator+(7);

using the ‘.’ style call that we associate with the use of member functions.

But what if we wanted to say

Date today = getTodaysDate();
Date aWeekFromToday = today + 7;  // today.operator+(7)
Date alsoAWeekFromToday = 7 + today;  

Can we handle the second addition via a member function? No, we can’t. Because if it were a member function, then the compiler would be trying to translate 7 + today to 7.operator+(today), and that’s not legal. Every member function must always take a (pointer to a) value of its class type on the left of the function name. “7” isn’t a Date. “7” can’t be the first parameter to a call to a Date member function.

If we want to support our addition in either order, we can combine a member function with an overloaded version going in the other direction:

class Date {
 private:
   int daysSinceEpoch;  
 public:
   Date ();
       ⋮ 
   Date operator+(int nDays) const;
};

inline Date operator+ (int nDays, const Date& date) 
{
   return date + nDays;  // call the member function operator+
}

4.3.2 operator<<

I recommend that you always provide an output operator for every class you write, even if you don’t expect to use it in your final application.

My reason for this is quite practical. Sooner or later, you’re going to discover that your program isn’t working right. So how are you going to debug it? The most common thing to do is to add debugging output at all the spots where you think things might be going wrong. So you come to some statement:

  doSomethingTo(book1);

and you realize that it might be really useful to know just what the value of that book was before and after the call:

  cerr << "Before doSomethingTo: " << book1 << endl;
  doSomethingTo(book1);
  cerr << "After doSomethingTo: " << book1 << endl;

Now, if you have already written that operator<< function for your Book class, you can proceed immediately. If you haven’t written it already, do you really want to be writing and debugging a new function now, when you are already dealing with a different bug?

To add an output operator, we can add the appropriate declaration

class Book {
private:
	std::string title;
	std::string isbn;

	static const int MaxAuthors = 12;
	Author* authors;
	int numAuthors;

public:
	Book();
	Book(const std::string& title, const std::string& isbn);

	std::string getTitle() const {return title;}
	void setTitle(std::string theTitle) {title = theTitle;}

	void addAuthor (const Author&);
	int numberOfAuthors() const;
  Author getAuthor(int index) const;

	std::string getISBN() const {return isbn;}
	void setISBN(std::string id) {isbn = id;}
};

std::ostream& operator<< (std::ostream& out, const Book& book);

The output operator must be declared as a standalone outside of the class. That’s because, when we write

cout << book1; 

the first parameter is cout, which is not a Book. cout is a C++ std::ostream. So operator<< could only be a member function of class std::ostream. And that’s no good for our purposes. So if we want to support output of Book or Date or any of our other classes, we need to do it as an standalone, non-member function.

Another unusual point: notice the return value on the operator<< declaration. Each implementation of operator<< is supposed to return the output stream to which we are writing. It’s the fact that << is an expression that returns the stream we are writing to that let’s us write “chains” of output expressions like:

cout << book1 << " is better than " << book2 << endl;

which is treated by the compiler as if we had written:

(((cout << book1) << " is better than ") << book2) << endl;

Look at the second <<.

So, the left-hand parameter needs to be the output stream that we are writing all of this output to.

By convention, then, every implementation of operator<< returns the stream that it has just written into, so that a chain of << calls will, each in turn, pass the stream on to the next one in line.


Output of private data

Because the output operator is a standalone function, it will not have access to the private data members of the class.

That means that the most straightforward implementation will not compile:

std::ostream& operator<< (const std::ostream& out, const Book& book)
{
  out << book.title << "\n"    // error: data member title is private
      << book.isbn  << "\n";   // error: data member isbn is private
  return out;
}

There are 3 ways to get around this problem.

  1. Restrict our function body to use of the public functions that provide access to the attributes of the class:

    std::ostream& operator<< (const std::ostream& out, const Book& book)
    {
      out << book.getTitle() << "\n"    
          << book.getIsbn()  << "\n";   
      return out;
    }
    
  2. Create a member function that knows how to print and that can be called from the operator:

    class Book {
    private:
       ⋮
    public:
       ⋮
      void print (std::ostream& out) const;
    };
    
    std::ostream& operator<< (const std::ostream& out, const Book& book)
    {
      book.print(out);
      return out;
    }
        ⋮
    void Book::print(const std::ostream& out) const
    {
      out << title << "\n" 
          << book  << "\n";
    }
    
  3. Make the operator a friend of the class. “Friends” in C++ have access to private data members;

    class Book {
    private:
       ⋮
       friend std::ostream& operator<< (const std::ostream& out, const Book& book);
    public:
       ⋮
    };
    
    std::ostream& operator<< (const std::ostream& out, const Book& book)
    {
      out << title << "\n" 
          << book  << "\n";
      return out;
    }
    

Which of these you choose is up to you. I tend to lean toward #3 myself, because it just makes sense that the output operator would be an member of the class, even though if the rules of the language say that we can’t declare it as one.