14.3 — Member functions

In lesson 13.7 -- Introduction to structs, members, and member selection, we introduced the struct program-defined type, which can contain member variables. Here is an example of a struct used to hold a date:

struct Date
{
    int year {};
    int month {};
    int day {};
};

Now, if we want to print the date to the screen (something we probably want to do a lot), it makes sense to write a function to do this. Here’s a full program:

#include <iostream>

struct Date
{
    // here are our member variables
    int year {};
    int month {};
    int day {};
};

void print(const Date& date)
{
    // member variables accessed using member selection operator (.)
    std::cout << date.year << '/' << date.month << '/' << date.day;
}

int main()
{
    Date today { 2020, 10, 14 }; // aggregate initialize our struct

    today.day = 16; // member variables accessed using member selection operator (.)
    print(today);   // non-member function accessed using normal calling convention

    return 0;
}

This program prints:

2020/10/16

The separation of properties and actions

Take a look around you -- everywhere you look are objects: books and buildings and food and even you. Real-life objects have two major components to them: 1) Some number of observable properties (e.g. weight, color, size, solidity, shape, etc…), and 2) Some number of actions that they can perform or have performed on them (e.g. being opened, damaging something else, etc…) based on those properties. These properties and actions are inseparable.

In programming, we represent properties with variables, and actions with functions.

In the Date example above, note that we have defined our properties (the member variables of Date) and the actions we perform using those properties (the function print()) separately. We are left to infer a connection between Date and print() based solely on the const Date& parameter of print().

While we could put both Date and print() into a namespace (to make it clearer that the two are meant to be packaged together), that adds yet more names into our program and more namespace prefixes, cluttering our code.

It sure would be nice if there were some way to define our properties and actions together, as a single package.

Member functions

In addition to having member variables, class types (which includes structs, classes, and unions) can also have their own functions! Functions that belong to a class type are called member functions.

As an aside…

In other object-oriented languages (such as Java and C#), these are called methods. Although the term “method” is not used in C++, programmers who learned one of those other languages first may still use that term.

Functions that are not member functions are called non-member functions (or occasionally free functions) to distinguish them from member functions. The print() function above is a non-member function.

Author’s note

In this lesson, we’ll use structs to show examples of member functions -- but everything we show here applies equally well to classes. For reasons that will become obvious when we get there, we’ll show examples of classes with member functions in upcoming lesson (14.5 -- Public and private members and access specifiers).

Member functions must be declared inside the class type definition, and can be defined inside or outside of the class type definition. As a reminder, a definition is also a declaration, so if we define a member function inside the class, that counts as a declaration.

To keep things simple, we’ll define our member functions inside the class type definition for now.

Related content

We show how to define member functions outside the class type definition in lesson 15.2 -- Classes and header files.

A member function example

Let’s rewrite the Date example from the top of the lesson, converting print() from a non-member function into a member function:

// Member function version
#include <iostream>

struct Date
{
    int year {};
    int month {};
    int day {};

    void print() // defines a member function named print
    {
        std::cout << year << '/' << month << '/' << day;
    }
};

int main()
{
    Date today { 2020, 10, 14 }; // aggregate initialize our struct

    today.day = 16; // member variables accessed using member selection operator (.)
    today.print();  // member functions also accessed using member selection operator (.)

    return 0;
}

This program compiles and produces the same result as above:

2020/10/16

There are three key differences between the non-member and member examples:

  1. Where we declare (and define) the print() function
  2. How we call the print() function
  3. How we access members inside the print() function

Let’s explore each of these in turn.

Member functions are declared inside the class type definition

In the non-member example, the print() non-member function is defined outside of the Date struct, in the global namespace. By default, it has external linkage, so it could be called from other source files (with the appropriate forward declaration).

In the member example, the print() member function is declared (and in this case, defined) inside the Date struct definition. Because print() is declared as part of the Date, this tells the compiler that print() is a member function.

Member functions defined inside the class type definition are implicitly inline, so they will not cause violations of the one-definition rule if the class type definition is included into multiple code files.

Related content

Member functions can also be (forward) declared inside the class definition, and defined after the class definition. We cover this in lesson 15.2 -- Classes and header files.

Calling member functions (and the implicit object)

In the non-member example, we call print(today), where today is (explicitly) passed as an argument.

In the member example, we call today.print(). This syntax, which uses the member selection operator (.) to select the member function to call, is consistent with how we access member variables (e.g. today.day = 16;).

All (non-static) member functions must be called using an object of that class type. In this case, today is the object that print() is being called on.

Note that in the member function case, we don’t need to pass today as an argument. The object that a member function is called on is implicitly passed to the member function. For this reason, the object that a member function is called on is often called the implicit object.

In other words, when we call today.print(), today is the implicit object, and it is implicitly passed to the print() member function.

Related content

We cover the mechanics of how the associated object is actually passed to a member function in lesson 15.1 -- The hidden “this” pointer and member function chaining.

Accessing members inside a member function uses the implicit object

Here’s the non-member version of print() again:

// non-member version of print
void print(const Date& date)
{
    // member variables accessed using member selection operator (.)
    std::cout << date.year << '/' << date.month << '/' << date.day;
}

This version of print() has reference parameter const Date& date. Within the function, we access the members through this reference parameter, as date.year, date.month, and date.day. When print(today) is called, the date reference parameter is bound to argument today, and date.year, date.month, and date.day evaluate to today.year, today.month, and today.day respectively.

Now let’s look at the definition of the print() member function again:

    void print() // defines a member function named print()
    {
        std::cout << year << '/' << month << '/' << day;
    }

In the member example, we access the members as year, month, and day.

Inside a member function, any member identifier that is not prefixed with the member selection operator (.) is associated with the implicit object.

In other words, when today.print() is called, today is our implicit object, and year, month, and day (which are not prefixed) evaluate to the values of today.year, today.month, and today.day respectively.

Key insight

With non-member functions, we have to explicitly pass an object to the function to work with, and members are explicitly accessed through that object.

With member functions, we implicitly pass an object to the function to work with, and members are implicitly accessed through that object.

Another member function example

Here’s an example with a slightly more complex member function:

#include <iostream>
#include <string>

struct Person
{
    std::string name{};
    int age{};

    void kisses(const Person& person)
    {
        std::cout << name << " kisses " << person.name << '\n';
    }
};

int main()
{
    Person joe{ "Joe", 29 };
    Person kate{ "Kate", 27 };

    joe.kisses(kate);

    return 0;
}

This produces the output:

Joe kisses Kate

Let’s examine how this works. First, we define two Person structs, joe and kate. Next, we call joe.kisses(kate). joe is the implicit object here, and kate is passed as an explicit argument.

When the kisses() member function executes, the identifier name doesn’t use the member selection operator (.), so it refers to the implicit object, which is joe. So this resolves to joe.name. person.name uses the member selection operator, so it does not refer to the implicit object. Since person is a reference for kate, this resolves to kate.name.

Key insight

Without a member function, we would have written kisses(joe, kate). With a member function, we write joe.kisses(kate). Note how much better the latter reads, and how it makes clear exactly which object is initiating the action and which is in support.

Member variables and functions can be defined in any order

The C++ compiler normally compiles code from top to bottom. For each name encountered, the compiler determines whether it has already seen a declaration for that name, so that it can do proper type checking.

Non-members must be declared before they can be used, or the compiler will complain:

int x()
{
    return y(); // error: y not declared yet, so compiler doesn't know what it is
}
 
int y()
{
    return 5;
}

To address this, we typically either define our non-members in rough order of use (which requires work whenever we need to change the order), or use forward declarations (which requires work to add).

However, inside a class definition, this restriction doesn’t apply: you can access member variables and member functions before they are declared. This means you can define member variables and member functions in any order you like!

For example:

struct Foo
{
    int z() { return m_data; } // We can access data members before they are defined
    int x() { return y(); }    // We can can access member functions before they are defined

    int m_data { y() };        // This even works in default member initializers (see warning below)
    int y() { return 5; }
};

We discuss a recommended order for member definition in upcoming lesson 14.8 -- The benefits of data hiding (encapsulation).

Warning

Data members are initialized in order of declaration. If the initialization of a data member accesses another data member that isn’t declared until later (and thus hasn’t been initialized yet), the initialization will result in undefined behavior.

struct Bad
{
    int m_bad1 { m_data }; // undefined behavior: m_bad1 initialized before m_data
    int m_bad2 { fcn() };  // undefined behavior: m_bad2 initialized before m_data (accessed through fcn())

    int m_data { 5 };
    int fcn() { return m_data; }
};

For this reason, it’s generally a good idea to avoid using other members inside default member initializers.

For advanced readers

To allow the definition of data members and member functions in any order, compilers employ a neat trick. When the compiler encounters a member function defined inside the class definition:

  • The member function is implicitly forward declared.
  • The member function definition is moved immediately after the end of the class definition.

That way, by the time the compiler actually compiles the member function definitions, it has already seen the full class definition (containing declarations for all members!)

For example, when the compiler encounters this:

struct Foo
{
    int z() { return m_data; } // m_data not declared yet
    int x() { return y(); }    // y not declared yet
    int y() { return 5; }

    int m_data{};
};

It will compile the equivalent of this:

struct Foo
{
    int z(); // forward declaration of Foo::z()
    int x(); // forward declaration of Foo::x()
    int y(); // forward declaration of Foo::y()

    int m_data{};
};

int Foo::z() { return m_data; } // m_data already declared above
int Foo::x() { return y(); }    // y already declared above
int Foo::y() { return 5; }

Member functions can be overloaded

Just like non-member functions, member functions can be overloaded, so long as each member function can be differentiated.

Related content

We cover function overload differentiation in lesson 11.2 -- Function overload differentiation.

Here’s an example of a Date struct with overloaded print() member functions:

#include <iostream>
#include <string_view>

struct Date
{
    int year {};
    int month {};
    int day {};

    void print()
    {
        std::cout << year << '/' << month << '/' << day;
    }

    void print(std::string_view prefix)
    {
        std::cout << prefix << year << '/' << month << '/' << day;
    }
};

int main()
{
    Date today { 2020, 10, 14 };

    today.print(); // calls Date::print()
    std::cout << '\n';

    today.print("The date is: "); // calls Date::print(std::string_view)
    std::cout << '\n';

    return 0;
}

This prints:

2020/10/14
The date is: 2020/10/14

Structs and member functions

In C, structs only have data members, not member functions.

In C++, while designing classes, Bjarne Stroustrup spent some amount of time considering whether structs (which were inherited from C) should be granted the ability to have member functions. Upon consideration, he determined that they should.

As an aside…

That decision led to a cascade of other questions about what other new C++ capabilities structs should have access to. Bjarne was concerned that giving structs access to a limited subset of capabilities would end up adding complexity and edge-cases to the language. For simplicity, he ultimately decided that structs and classes would have a unified ruleset (meaning structs can do everything classes can, and vice-versa), and convention could dictate how structs would actually be used.

In modern C++, it is fine for structs to have member functions. This excludes constructors, which are a special type of member function that we cover in upcoming lesson 14.9 -- Introduction to constructors. A class type with a constructor is no longer an aggregate, and we want our structs to remain aggregates.

Best practice

Member functions can be used with both structs and classes.

However, structs should avoid defining constructor member functions, as doing so makes them a non-aggregate.

Class types with no data members

It is possible to create class types with no data members (e.g. class types that only have member functions). It is also possible to instantiate objects of such a class type:

#include <iostream>

struct Foo
{
    void printHi() { std::cout << "Hi!\n"; }
};

int main()
{
    Foo f{};
    f.printHi(); // requires object to call

    return 0;
}

However, if a class type does not have any data members, then using a class type is probably overkill. In such cases, consider using a namespace (containing non-member functions) instead. This makes it clearer to the reader that no data is being managed (and does not require an object to be instantiated to call the functions).

#include <iostream>

namespace Foo
{
    void printHi() { std::cout << "Hi!\n"; }
};

int main()
{
    Foo::printHi(); // no object needed

    return 0;
}

Best practice

If your class type has no data members, prefer using a namespace.

Quiz time

Question #1

Create a struct called IntPair that holds two integers. Add a member function named print that prints the value of the two integers.

The following program function should compile:

#include <iostream>

// Provide the definition for IntPair and the print() member function here

int main()
{
	IntPair p1 {1, 2};
	IntPair p2 {3, 4};

	std::cout << "p1: ";
	p1.print();

	std::cout << "p2: ";
	p2.print();

	return 0;
}

and produce the output:

p1: Pair(1, 2)
p2: Pair(3, 4)

Show Solution

Question #2

Add a new member function to IntPair named isEqual that returns a Boolean indicating whether one IntPair is equal to another.

The following program function should compile:

#include <iostream>

// Provide the definition for IntPair and the member functions here

int main()
{
	IntPair p1 {1, 2};
	IntPair p2 {3, 4};

	std::cout << "p1: ";
	p1.print();

	std::cout << "p2: ";
	p2.print();

	std::cout << "p1 and p1 " << (p1.isEqual(p1) ? "are equal\n" : "are not equal\n");
	std::cout << "p1 and p2 " << (p1.isEqual(p2) ? "are equal\n" : "are not equal\n");

	return 0;
}

and produce the output:

p1: Pair(1, 2)
p2: Pair(3, 4)
p1 and p1 are equal
p1 and p2 are not equal 

Show Solution

guest
Your email address will not be displayed
Find a mistake? Leave a comment above!
Correction-related comments will be deleted after processing to help reduce clutter. Thanks for helping to make the site better for everyone!
Avatars from https://gravatar.com/ are connected to your provided email address.
Notify me about replies:  
429 Comments
Newest
Oldest Most Voted
Inline Feedbacks
View all comments