14.5 — Access functions

In previous lesson 14.4 -- Public and private members and access specifiers, we discussed the public and private access levels. As a reminder, classes typically make their data members private, and private members can not be directly accessed by the public.

Consider the following Date class:

#include <iostream>

class Date
{
private:
    int m_year{ 2020 };
    int m_month{ 10 };
    int m_day{ 14 };

public:
    void print()
    {
        std::cout << m_year << '/' << m_month << '/' << m_day << '\n';
    }
};

int main()
{
    Date d{};  // create a Date object
    d.print(); // print the date

    return 0;
}

While this class provides a print() member function to print the entire date, this may not be sufficient for what the user wants to do. For example, what if the user of a Date object wanted to get the year? Or to change the year to a different value? They would be unable to do so, as m_year is private (and thus can’t be directly accessed by the public).

For some classes, it can be appropriate (in the context of what the class does) for us to be able to get or set the value of a private member variable.

Access functions

An access function is a trivial public member function whose job is to retrieve or change the value of a private member variable.

Access functions come in two flavors: getters and setters. Getters (also sometimes called accessors) are public member functions that return the value of a private member variable. Setters (also sometimes called mutators) are public member functions that set the value of a private member variable.

For illustrative purposes, let’s update our Date class to have a full set of getters and setters:

#include <iostream>

class Date
{
private:
    int m_year { 2020 };
    int m_month { 10 };
    int m_day { 14 };

public:
    void print()
    {
        std::cout << m_year << '/' << m_month << '/' << m_day << '\n';
    }

    int getYear() { return m_year; }              // getter for year
    void setYear(int year) { m_year = year; }     // setter for year

    int getMonth() { return m_month; }            // getter for month
    void setMonth(int month) { m_month = month; } // setter for month

    int getDay() { return m_day; }                // getter for day
    void setDay(int day) { m_day = day; }         // setter for day
};

int main()
{
    Date d{};
    d.setYear(2021);
    std::cout << "The year is: " << d.getYear() << '\n';

    return 0;
}

This prints:

The year is: 2021

Access function naming

There is no common convention for naming access functions. However, there are two naming conventions that are more popular than others.

  • Prefixed with “get” or “set”:
    int getDay() { return m_day; }        // getter
    void setDay(int day) { m_day = day; } // setter

The advantage of using “get” and “set” prefixes is that it makes it clear that these are access functions (and should be inexpensive to call).

  • No prefix:
    int day() { return m_day; }        // getter
    void day(int day) { m_day = day; } // setter

This style is more concise, and uses the same name for both the getter and setter (relying on function overloading to provide this capability). The C++ standard library uses this convention.

The downside of the no-prefix convention is that it is not particularly obvious that this is setting the value of the day member:

d.day(5); // does this look like it's setting the day member to 5?

We’ll prefer the “get” and “set” prefix in these tutorials, but feel free to use whichever convention you prefer.

Key insight

One of the best reasons to prefix private data members with “m_” is to avoid having data members and getters with the same name (something C++ doesn’t support, although other languages like Java do).

Getters should return by value or by const lvalue reference

Getters should provide “read-only” access to data. Therefore, the best practice is that they should return by either value (if making a copy of the member is inexpensive) or by const lvalue reference (if making a copy of the member is expensive).

Here is an example of a getter that returns by const reference:

#include <iostream>
#include <string>

class Employee
{
	std::string m_name{};

public:
	void setName(std::string_view name) { m_name = name; }
	const std::string& getName() { return m_name; } //  getter returns by const reference
};

int main()
{
	Employee joe;
	joe.setName("Joe");
	std::cout << joe.getName();

	return 0;
}

In this case, returning a copy of std::string would be expensive, so we return by const reference rather than by value.

The return type of a getter should match the type of the data member. In the above example, m_name is of type std::string, so getName() returns const std::string& instead of std::string_view. Returning a std::string_view would require a temporary std::string_view to be created and returned every time the function was called. That’s needlessly inefficient.

Best practice

Getters should return by value or const lvalue reference. The type of the returned object should match the data member, to avoid unnecessary conversions.

In previous lessons, we mentioned that you should not have a function return a reference to a local variable, because the reference will be left dangling when the local variable is destroyed at the end of the function. Data members are not local variables -- they exist as long as the class type object that contains them is alive. Therefore, returning a reference to a data member is safe as long as the implicit object is not destroyed immediately. The easiest way to guarantee this is to ensure the function returning the reference is called on an lvalue (such as a variable). This is the case most of the time.

In the above example, joe.getName() returns a reference to joe.m_name, which the caller then prints to the console. This is okay because variable joe exists in the scope of the caller until the end of the main() function. Thus any returned references to data members will be valid for that same duration.

Key insight

It is okay to return a (const) lvalue reference to a data member, because an lvalue implicit object (containing the data member) will still exist in the scope of the caller.

For advanced readers

Because they can be called on const objects, getters should really be const member functions. We cover const member functions in lesson 15.2 -- Const class objects and const member functions.

    int day() const { return m_day; }  // const getter member function (can be called on both const and non-const objects)

Setters do not need to be made const because you can’t modify the data of a const object anyway.

Return by reference and rvalue implicit objects

When the implicit object is an rvalue (such as the return value of a function that returns by value), we have to be more careful with functions that return data members by reference. This is because an rvalue implicit object will be destroyed at the end of the full expression in which it is created, and any references to members of that rvalue will be left dangling.

Therefore, a reference returned from an rvalue implicit object can only be safely used within the same expression where the rvalue object is created.

For example:

#include <iostream>
#include <string>
#include <string_view>

class Employee
{
	std::string m_name{};

public:
	void setName(std::string_view name) { m_name = name; }
	const std::string& getName() { return m_name; } //  getter returns by const reference
};

// createEmployee() returns an Employee by value (which means the returned value is an rvalue)
Employee createEmployee(std::string_view name)
{
	Employee e;
	e.setName(name);
	return e;
}

int main()
{
	std::cout << createEmployee("Frank").getName(); // okay, reference used in same expression

	const std::string& ref = createEmployee("Garbo").getName(); // reference becomes dangling when return value of getEmployee() is destroyed
	std::cout << ref; // undefined behavior

	return 0;
}

Because calling member functions that return a reference on an rvalue implicit object is potentially dangerous, we suggest avoiding doing so altogether.

Warning

Avoid calling member functions that return by reference from rvalue implicit objects, as the reference will be left dangling when the rvalue implicit object is destroyed at the end of the full expression in which it is created.

For advanced readers

We can enlist the compiler’s help to prevent a member function that returns a reference from being called on an rvalue object by making use of a little-known feature of C++ called a ref-qualifier. Here’s the same example as above using ref-qualifiers:

#include <iostream>
#include <string>
#include <string_view>

class Employee
{
	std::string m_name{};

public:
	void setName(std::string_view name) { m_name = name; }

	// Overload our getName() member function
	// The version using the & qualifier will prefer matching lvalue objects
	// The version using the && qualifier will prefer matching rvalue objects
	// = delete tells the compiler that calling this function should emit a compilation error
	const std::string& getName() & { return m_name; } // when called on an lvalue, return a reference to the member
	const std::string& getName() && = delete;         // when called on an rvalue, emit a compilation error
};

// createEmployee() returns an Employee by value (which means the returned value is an rvalue)
Employee createEmployee(std::string_view name)
{
	Employee e;
	e.setName(name);
	return e;
}

int main()
{
	Employee joe{};
	joe.setName("Joe");
	std::cout << joe.getName(); // okay, Joe is an lvalue, so this calls std::string& getName() &
    
	std::cout << createEmployee("Frank").getName(); // compile error: std::string& getName() && is deleted

	const std::string& ref = createEmployee("Garbo").getName(); // compile error: std::string& getName() && is deleted
	std::cout << ref; // undefined behavior

	return 0;
}

Doing this for every getter that returns by reference adds a lot of clutter, so you will have to weigh whether it’s worth the clutter for the added protection.

Do not return non-const references to private data members

Because a reference acts just like the object being reference, a member function that returns a non-const reference provides direct access to that member (even if the member is private).

For example:

#include <iostream>

class Foo
{
private:
    int m_value{ 4 }; // private member

public:
    int& value() { return m_value; } // returns a non-const reference (don't do this)
};

int main()
{
    Foo f{};                // f.m_value is initialized to default value 4
    f.value() = 5;          // The equivalent of m_value = 5
    std::cout << f.value(); // prints 5

    return 0;
}

Because value() returns a non-const reference to m_value, the caller is able to use that reference to directly access (and change the value of) m_value.

This allows the caller to subvert the access control system.

Access functions concerns

There is a fair bit of discussion around cases in which access functions should be used or avoided. Many developers would argue that use of access functions violates good class design (a topic that could easily fill an entire book).

For now, we’ll recommend a pragmatic approach. As you create your classes, consider the following:

  • If your class has no invariants and requires a lot of access functions, consider using a struct and providing direct access to members instead.
  • Prefer implementing behaviors or actions instead of access functions. For example, instead of a setAlive(bool) setter, implement a kill() function.
  • Only provide access functions in cases where the public would reasonably need to get or set the value of an individual member.

Why make data private if we’re going to provide a public access functions to it?

So glad you asked. We’ll cover this topic in the very next lesson (14.6 -- The benefits of data hiding (encapsulation))!

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:  
207 Comments
Newest
Oldest Most Voted
Inline Feedbacks
View all comments