How to write C++ getters and setters

81,422

Solution 1

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.

Solution 2

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.

Solution 3

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.

Share:
81,422
bolov
Author by

bolov

Updated on July 09, 2022

Comments

  • bolov
    bolov over 1 year

    If I need to write a setter and/or getter for I write it like this:

    struct X { /*...*/};
    class Foo
    {
    private:
        X x_;
    public:
        void set_x(X value)
        {
            x_ = value;
        }
        X get_x()
        {
            return x_;
        }
    };
    

    However I have heard that this is the Java style of writing setters and getters and that I should write it in C++ style. Moreover I was told it is ineficient and even incorrect. What does that mean? How can I write the setters and getters in C++?


    Assume the need for getters and/or setters is justified. E.g. maybe we do some checks in the setter, or maybe we write only the getter.

    There has been a lot of chatter about not needing getters and setters. While I agree with most of what's been said here, I still avocate for the need to know how to idiomatically write such methods because there are legitimate reasons where getters and setters are the right solution. They might not look at first glance as a setter or getter but they are, or at least the pattern for writing them applies.

    E.g.:

    • Getting the size of a vector. You don't want to expose a data member, because it needs to be read only.

    • Getters and setters don't need to just expose a data member. Think about getting and setting an element of an array. There is logic there, you can't just expose a data member, there is no data member to expose in the first place. It's still a getter/setter pair you can't avoid:

      class Vector
      {
          void set_element(std::size_t index, int new_value);
          int get_element(std::size_t index);
      };
      

      Knowing the C++ idiomatic way of writing getters and setters will allow me to write the above get_element/set_element in a C++ idiomatic way.

  • rubenvb
    rubenvb over 5 years
    A simple setter should accept its argument by value and move from that to handle temporaries better.
  • NathanOliver
    NathanOliver over 5 years
    Why is your getter giving a reference? You are breaking encapsulation by doing so. even passing out a const& is a problem as I can cast the constness away and now I have direct access to the member.
  • rubenvb
    rubenvb over 5 years
    @Nathan If you are saying something breaks encapsulation but you're using const_cast to do so your argument has little value. There's a whole lot more you can do to break encapsulation even if your getter didn't return a const reference.
  • bolov
    bolov over 5 years
    @NathanOliver if you cast the constness away you are doing something you shouldn't be doing.
  • NathanOliver
    NathanOliver over 5 years
    @bolov Why? It is perfectly legal to cast constness away from something that isn't const.
  • StoryTeller - Unslander Monica
    StoryTeller - Unslander Monica over 5 years
    @bolov - I do understand that. Which is why I focused on what I found confusing. I consider myself fairly well-versed in C++, how would a novice handle it?
  • Caleth
    Caleth over 5 years
    @NathanOliver but how do you know that a const Foo & is bound to a non-const Foo?
  • bolov
    bolov over 5 years
    @NathanOliver legal (in the specified case) yes. But you shouldn't do it. If I give you a const reference there is a reason for that. You shouldn't cast away the reference (although it might be legal). Plus you don't know if the data member is const which would make it UB.
  • bolov
    bolov over 5 years
    @StoryTeller I understand your point about being confusing. I ... don't know to structure it in a better way. And I briefly touched about references in the first paragraph.
  • StoryTeller - Unslander Monica
    StoryTeller - Unslander Monica over 5 years
    I deleted my comments now, since you did some extensive editing. IMO, the answer could benefit from keeping trailing return types until the end. Since they really are mostly a stylistic point in the current explanation. I imagine novices would find that to be needless complexity to their emerging ability to parse C++ declarations. It doesn't really add anything to the other valid points.
  • StoryTeller - Unslander Monica
    StoryTeller - Unslander Monica over 5 years
    You know, another point to consider, since you touched on move semantics, is that one can overload member functions on ref qualifiers too. So we can have auto x() && -> X&& { return std::move(x_); } and allow that efficiency we supposedly gave up on when starting.
  • Karl Nicoll over 5 years
    Doesn't this violate one of the criteria of the question? "e.g. maybe we do some checks in the setters" AFAICT this doesn't work for a non-const getter/setter X& x() which would allow Foo:x_ to be reassigned or mutated without checking post-conditions.
  • bolov
    bolov over 5 years
    @KarlNicoll X can be a proxxy.
  • Karl Nicoll over 5 years
    Can you provide an example of where each type of property is used in the standard library?
  • Caleth
    Caleth over 5 years
    @KarlNicoll I've got a related answer at Software Engineering that has examples
  • 463035818_is_not_a_number
    463035818_is_not_a_number about 3 years
    i suppose the difference between "identity" and "value oriented" is whether data is actually encapsulated. Only in the second Foo one coudl for example count how often x was modified, while in the first the "getters" can only do things that are optional for the caller, like eg at vs direct access
  • Caleth
    Caleth about 3 years
    @idclev463035818 Sometimes you don't want encapsulated. std::vector would be quite obnoxious if you couldn't reference the elements.
  • Lou
    Lou over 2 years
    One newbie question: why doesn't the constructor for checker accept std::function<T(T const &)>, but instead uses template <class checker>?
  • Bob
    Bob over 1 year
    Perhaps, for simple stuff, getters and setters are a mistake - adding more complexity than is needed, before there's any demand to do so, so you're just opening up a new source for error without any immediate benefit. But consider multi-threaded code, where you need to defend variables from the "multiple update" problem or wrap them in a critical section. At that point, getters and setters become the easy route, that doesn't require demanding specific usage patterns (e.g. "you must lock this before you can call this" type rules). Doing it early is a mistake, but it has good and valid use too.
  • Jerry Coffin
    Jerry Coffin over 1 year
    @Bob In my opinion, for multithreading something like I've shown above with the checked class is the easy route--both easy to write and (contrary to explicit getters/setters) easy and clean to use.