Member Functions Returning References to Data Members

Getter functions typically return copies of member variables. But what if you want to allow modification through a reference?

Returning by value vs. by reference

Standard getter returns a copy:

class Box
{
private:
    int contents{};

public:
    int getContents() const
    {
        return contents;  // Returns a copy
    }
};

int main()
{
    Box box{};
    int value{ box.getContents() };
    value = 100;  // Modifies local copy, not box.contents

    return 0;
}

Returning by reference allows direct access:

class Box
{
private:
    int contents{};

public:
    int& getContents()
    {
        return contents;  // Returns a reference
    }
};

int main()
{
    Box box{};
    box.getContents() = 100;  // Modifies box.contents directly!

    return 0;
}

The danger

Returning non-const references to private members defeats encapsulation:

class BankAccount
{
private:
    double balance{};

public:
    double& getBalance()
    {
        return balance;  // BAD: Exposes internal state
    }
};

int main()
{
    BankAccount account{};
    account.getBalance() = 1000000.0;  // Bypasses all validation!

    return 0;
}

This is essentially making the member public. The private access specifier becomes meaningless.

When it's acceptable

Returning const references to large objects avoids expensive copies:

class Library
{
private:
    std::string bookTitle{};

public:
    const std::string& getTitle() const
    {
        return bookTitle;  // OK: const reference, read-only
    }
};

The const prevents modification, so encapsulation remains intact. This is safe and efficient for large objects like strings.

Rvalue implicit objects and dangling references

When calling a member function on an lvalue object, the returned reference remains valid as long as the object exists. But what happens when the implicit object is an rvalue (a temporary)?

Rvalue objects are destroyed at the end of the full expression in which they are created. Any references to members of that rvalue become dangling at that point.

#include <iostream>
#include <string>

class Employee
{
private:
    std::string m_name{};

public:
    void setName(std::string_view name) { m_name = name; }
    const std::string& getName() const { return m_name; }
};

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

int main()
{
    // Case 1: OK - use returned reference immediately in same expression
    std::cout << createEmployee("Frank").getName() << '\n';

    // Case 2: BAD - saving reference to member of rvalue creates dangling reference
    const std::string& ref{ createEmployee("Garbo").getName() }; // reference becomes dangling
    std::cout << ref << '\n'; // undefined behavior!

    // Case 3: OK - copy the value to a local variable
    std::string val{ createEmployee("Hans").getName() }; // makes a copy
    std::cout << val << '\n'; // safe: val is independent of the temporary

    return 0;
}

In Case 1, the temporary Employee returned by createEmployee() exists until the end of the full expression. We use the reference immediately to print the name, which is safe.

In Case 2, we save the returned reference into a local reference variable. The temporary Employee is destroyed at the end of the full expression (the initialization), leaving ref dangling. Using ref afterward is undefined behavior.

In Case 3, we copy the referenced value into a non-reference local variable. The copy is independent of the temporary, so it remains valid after the temporary is destroyed.

Warning
A reference to a member of an rvalue object can only be safely used within the full expression where the rvalue is created. Do not save such references for later use.
Best Practice
Prefer to use the return value of a member function that returns by reference immediately. If you need to persist the value, copy it into a non-reference local variable rather than saving the reference.

Chain modification pattern

Sometimes you want a fluent interface where operations can chain:

class Transform
{
private:
    double value{};

public:
    Transform& scale(double factor)
    {
        value *= factor;
        return *this;  // Return reference to self
    }

    Transform& add(double amount)
    {
        value += amount;
        return *this;
    }

    double getValue() const { return value; }
};

int main()
{
    Transform t{};
    t.scale(2.0).add(5.0).scale(3.0);  // Chaining
    std::cout << t.getValue();

    return 0;
}

Here, returning a reference to the object itself (*this) allows method chaining. This is fine because you're not exposing private data directly.

Returning references to members of complex types

For container-like classes, returning references allows element modification:

class IntArray
{
private:
    int data[10]{};

public:
    int& operator[](int index)
    {
        return data[index];  // Allows modification
    }

    const int& operator[](int index) const
    {
        return data[index];  // Read-only for const objects
    }
};

int main()
{
    IntArray arr{};
    arr[0] = 42;              // Modify through reference
    std::cout << arr[0];      // Read value

    return 0;
}

This is acceptable because:

  • It matches expected container behavior
  • Users understand they're modifying the container's elements
  • You still control how elements are accessed (through operator[])
Best Practice
Prefer returning by const reference for read-only access to large objects. Avoid returning non-const references to members as it usually breaks encapsulation. Return references to self (*this) for method chaining. For small types, return by value since copying is cheap. For large types, return by const reference to avoid expensive copies.

Summary

Returning by value vs. reference: Returning by value creates a copy of the member, while returning by reference provides direct access to the member variable itself.

The danger of non-const references: Returning non-const references to private members defeats encapsulation by allowing external code to modify internal state directly, bypassing validation and control mechanisms.

Const references are safe: Returning const references allows efficient access to large objects without copying while preventing modification, preserving encapsulation.

Rvalue objects and dangling references: When a member function returning a reference is called on an rvalue (temporary) object, the reference becomes dangling when the temporary is destroyed at the end of the full expression. Use such references immediately or copy the value to a local variable.

Method chaining: Returning a reference to *this enables fluent interfaces where multiple operations can be chained together in a single statement.

Container exceptions: For container-like classes (arrays, lists), returning references to elements is acceptable and expected behavior, matching how standard containers work.

Encapsulation's value lies in controlling access to internal state. When you return non-const references to private members, you bypass validation logic, prevent future implementation changes, and make debugging harder. Keep private members truly private by returning values or const references.