How to write C++ getters and setters

Over the years, I've come to believe that the whole notion of getter/setter is usually a mistake. As contrary as it may sound, a public variable is normally the correct answer.

The trick is that the public variable should be of the correct type. In the question you've specified that either we've written a setter that does some checking of the value being written, or else that we're only writing a getter (so we have an effectively const object).

I would say that both of those are basically saying something like: "X is an int. Only it's not really an int--it's really something sort of like an int, but with these extra restrictions..."

And that brings us to the real point: if a careful look at X shows that it's really a different type, then define the type that it really is, and then create it as a public member of that type. The bare bones of it might look something like this:

template <class T>
class checked {
    T value;
    std::function<T(T const &)> check;

public:
    template <class checker>
    checked(checker check) 
        : check(check)
        , value(check(T())) 
    { }

    checked &operator=(T const &in) { value = check(in); return *this; }

    operator T() const { return value; }

    friend std::ostream &operator<<(std::ostream &os, checked const &c) {
        return os << c.value;
    }

    friend std::istream &operator>>(std::istream &is, checked &c) {
        try {
            T input;
            is >> input;
            c = input;
        }
        catch (...) {
            is.setstate(std::ios::failbit);
        }
        return is;
    }
};

This is generic, so the user can specify something function-like (e.g., a lambda) that assures the value is correct--it might pass the value through unchanged, or it might modify it (e.g., for a saturating type) or it might throw an exception--but if it doesn't throw, what it returns must be a value acceptable for the type being specified.

So, for example, to get an integer type that only allows values from 0 to 10, and saturates at 0 and 10 (i.e., any negative number becomes 0, and any number greater than 10 becomes 10, we might write code on this general order:

checked<int> foo([](auto i) { return std::min(std::max(i, 0), 10); });

Then we can do more or less the usual things with a foo, with the assurance that it will always be in the range 0..10:

std::cout << "Please enter a number from 0 to 10: ";
std::cin >> foo; // inputs will be clamped to range

std::cout << "You might have entered: " << foo << "\n";

foo = foo - 20; // result will be clamped to range
std::cout << "After subtracting 20: " << foo;

With this, we can safely make the member public, because the type we've defined it to be is really the type we want it to be--the conditions we want to place on it are inherent in the type, not something tacked on after the fact (so to speak) by the getter/setter.

Of course, that's for the case where we want to restrict the values in some way. If we just want a type that's effectively read-only, that's much easier--just a template that defines a constructor and an operator T, but not an assignment operator that takes a T as its parameter.

Of course, some cases of restricted input can be more complex. In some cases, you want something like a relationship between two things, so (for example) foo must be in the range 0..1000, and bar must be between 2x and 3x foo. There are two ways to handle things like that. One is to use the same template as above, but with the underlying type being a std::tuple<int, int>, and go from there. If your relationships are really complex, you may end up wanting to define a separate class entirely to define the objects in that complex relationship.

Summary

Define your member to be of the type you really want, and all the useful things the getter/setter could/would do get subsumed into the properties of that type.


There are two distinct forms of "properties" that turn up in the standard library, which I will categorise as "Identity oriented" and "Value oriented". Which you choose depends on how the system should interact with Foo. Neither is "more correct".

Identity oriented

class Foo
{
     X x_;
public:
          X & x()       { return x_; }
    const X & x() const { return x_; }
}

Here we return a reference to the underlying X member, which allows both sides of the call site to observe changes initiated by the other. The X member is visible to the outside world, presumably because it's identity is important. It may at first glance look like there is only the "get" side of a property, but this is not the case if X is assignable.

 Foo f;
 f.x() = X { ... };

Value oriented

class Foo
{
     X x_;
public:
     X x() const { return x_; }
     void x(X x) { x_ = std::move(x); }
}

Here we return a copy of the X member, and accept a copy to overwrite with. Later changes on either side do not propagate. Presumably we only care about the value of x in this case.


This is how I would write a generic setter/getter:

class Foo
{
private:
    X x_;

public:
    X&       x()        { return x_; }
    const X& x() const  { return x_; }
};

I will try to explain the reasoning behind each transformation:

The first issue with your version is that instead of passing around values you should pass const references. This avoids the needless copying. True, since C++11 the value can be moved, but that is not always possible. For basic data types (e.g. int) using values instead of references is OK.

So we first correct for that.

class Foo1
{
private:
    X x_;

public:
    void set_x(const X& value)
//             ^~~~~  ^
    {
        x_ = value;
    }

    const X& get_x()
//  ^~~~~  ^
    {
        return x_;
    }
};

Still there is a problem with the above solution. Since get_x does not modify the object it should be marked const. This is part of a C++ principle called const correctness.

The above solution will not let you get the property from a const object:

const Foo1 f;

X x = f.get_x(); // Compiler error, but it should be possible

This is because get_x not being a const method cannot be called on a const object. The rational for this is that a non-const method can modify the object, thus it is illegal to call it on a const object.

So we make the necessary adjustments:

class Foo2
{
private:
    X x_;

public:
    void set_x(const X& value)
    {
        x_ = value;
    }

    const X& get_x() const
//                   ^~~~~
    {
        return x_;
    }
};

The above variant is correct. However in C++ there is another way of writting it that is more C++ ish and less Java ish.

There are two things to consider:

  • we can return a reference to the data member and if we modify that reference we actually modify the data member itself. We can use this to write our setter.
  • in C++ methods can be overloaded by consteness alone.

So with the above knowledge we can write our final elegant C++ version:

Final version

class Foo
{
private:
    X x_;

public:
    X&       x()        { return x_; }
    const X& x() const  { return x_; }
};

As a personal preference I use the new trailing return function style. (e.g. instead of int foo() I write auto foo() -> int.

class Foo
{
private:
    X x_;

public:
    auto x()       -> X&       { return x_; }
    auto x() const -> const X& { return x_; }
};

And now we change the calling syntax from:

Foo2 f;
X x1;

f.set_x(x1);
X x2 = f.get_x();

to:

Foo f;
X x1;

f.x() = x1;
X x2 = f.x();
const Foo cf;
X x1;

//cf.x() = x1; // error as expected. We cannot modify a const object
X x2 = cf.x();

Beyond the final version

For performance reasons we can go a step further and overload on && and return an rvalue reference to x_, thus allowing moving from it if needed.

class Foo
{
private:
    X x_;

public:
    auto x() const& -> const X& { return x_; }
    auto x() &      -> X&       { return x_; }
    auto x() &&     -> X&&      { return std::move(x_); }

};

Many thanks for the feedback received in comments and particularly to StorryTeller for his great suggestions on improving this post.