How to choose between a setter and a reference-getter?

Author: Chloé Lourseyre

Context

When you implement a class, you often have to implement accessors to one or several attributes of this class to edit them.

There are two ways to do that:

  • A setter, a method that takes the new value for the attribute as argument.
  • A reference-getter, a method that returns a reference to the attribute itself.

Here is a short example that shows how to access the attribute bar with each method.

class Foo {
    int m_bar;

public:
    // Setter
    void set_bar(int bar) {
        m_bar = bar;
    }

    // Reference-getter
    int & get_bar() {
        return m_bar;
    }
};

int main() {
    Foo foo;

    // Editing via setter
    foo.set_bar(42);

    // Editing via reference-getter
    foo.get_bar() = 84;

    return 0;
}

Some may argue that there are more methods to do that, but I assert that they are just variations of one of these two.

The setter

A setter is a write-only interface to a class. You provide a value, and the class is updated accordingly.

Often, it will more or less directly update an attribute by copying/moving the data you provide.

Examples

// Most simple setter
void set_foo(int foo) {
    m_foo = foo;
}
// A setter that performs a test before edition
void set_foo(Foo foo) {
    if (foo.is_valid())
        m_foo = foo;
}
// A move-setter
void set_big_foo(BigFoo && big_foo) {
    m_big_foo = std::forward<BigFoo>(big_foo);
}

The reference-getter

A reference-getter is a method that directly returns the reference to an attribute to access and edit it.

This is peculiarly useful on object attributes so you can call non-const methods directly on them.

Examples

// Here is the implementation of the reference-getter
Foo & MyClass::get_foo() {
    return m_foo;
}

// ...

// Used to edit an attribute
myClass.getFoo().bar = 42;

// Used to call a non-const method
myClass.getFoo().udpate();

How to choose

Well, this is pretty simple as soon as you understand the difference.

The setter is required when you need to re-create the data and put it in the place of the existing one. This is suited when editing simple data (integers, floating values, pointers, etc.) or if you require a brand new value. Also, use setters if you explicitly want the user to be unable to read the data, only edit it.

The reference-getter is required when it is the attribute’s data that is edited (and not the attribute as a whole). Often, you will edit a part of the attribute or call a non-const method over it.

In other words, a setter replaces the attributes, and the reference-getter edits the attribute.

Example

Take the following code:

#include <vector>

using namespace std;

struct Item
{
    bool validity;
    int value;
};

class Foo
{
public:
    Foo(size_t size) :
        m_max_size(size),
        m_data(size, {true, 0})
    {}

    void set_max_size(size_t max_size) {
        m_max_size = max_size;
    }

    Item & get_item(size_t index) {
        return m_data.at(index);
    }

    size_t get_data_size() const {
        return m_data.size();
    }

private:
    bool m_max_size;
    std::vector<Item> m_data;
};

static void set_foo_size(Foo & foo, size_t new_size)
{
    foo.set_max_size(new_size);
    for (size_t i = new_size ; i < foo.get_data_size() ; ++i)
        foo.get_item(i).validity = false;
}

There, we have a simple little class that holds a collection of data (Items). These data can be valid or invalid (true is valid, false is invalid).

Then we implement a little function that resizes the collection without deleting any element. Out-of-bounds elements are made invalid instead.

We access the value m_max_size using a setter because it’s a plain-old integer that is replaced when the collection is resized.

We access each Item of m_data using a reference-getter because we do not always want to erase the item, sometimes we just want to edit a part of it.

Alternative

We could have edited the validity using a more specific setter, like this:

class Foo {

        // ...

        void set_item_validity(size_t index, bool validity) {
                m_data.at(index).validity = validity;
        }

        // ...

};

Doing so would prevent us from editing the value of the Item, so this decision entirely depends on your implementation details.

However, please consider bad practice to provide setters to both value and validity. Doing so for a 2-attributes data bucket may not be that inconvenient, but as soon as your implementation grows your codebase will be polluted by useless accessors. You need full access? Then get full access.

Wrapping up

This may look like a trivial subject to many of you, but there are a lot of developers that are still confused and use one when they need the other. Stay vigilant.

Thanks for reading and see you next week!

Author: Chloé Lourseyre

2 thoughts on “How to choose between a setter and a reference-getter?”

  1. the alternative is definitely my pref. I prefer downstream “setter”(which is a big word) and in a nutshell, let the obj deal with its own stuff instead of doing get by ref and then mocking with it)

Leave a Reply