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.