Thoughts about getters and setters in C++

This article deals with getters and setters in C++. I am sorry, it is not about coroutine, but part 2 of thread pools will come in the following weeks.

TL;DR: Getters and setters are bad for structure like objects.

Introduction

In this article, I will only give my opinion about them, I don’t want to offend anyone, I am just going to explain why, or why not, use getters and setters. I would be glad to have any debates in the comments part.

Just to be clear about what I am talking about when I talk about getter, I talk about a function that just returns something, and when I talk about setter, I talk about a function that just modifies one internal value, not doing any verification or other computations.

Getter performances

Let’s say we have a simple structure with their usual getter and setters:

class PersonGettersSetters {
  public:
    std::string getLastName() const { return m_lastName; }
    std::string getFirstName() const { return m_firstName; }
    int getAge() const {return m_age; }
    
    void setLastName(std::string lastName) { m_lastName = std::move(lastName); }
    void setFirstName(std::string firstName) { m_firstName = std::move(firstName); }
    void setAge(int age) {m_age = age; }
  private:
    int m_age = 26;
    std::string m_firstName = "Antoine";
    std::string m_lastName = "MORRIER";    
};

Let’s compare this version with the one without getters and setters.

struct Person {
    int age = 26;
    std::string firstName = "Antoine";
    std::string lastName = "MORRIER";
};

It is incredibly shorter and less error_prone. We can not make the error to return the last name instead of the first name.

Both codes are fully functional. We have a class Person with the first name, the last name, and an age. However, let’s say we want a function that returns the presentation of a person.

std::string getPresentation(const PersonGettersSetters &person) {
  return "Hello, my name is " + person.getFirstName() + " " + person.getLastName() +
  " and I am " + std::to_string(person.getAge());
}

std::string getPresentation(const Person &person) {
  return "Hello, my name is " + person.firstName + " " + person.lastName + " and I am " + std::to_string(person.age);
}
Show the performance differences between computation using getters by value and public access.

The version without getters performs this task 30% quicker than the version with getters. But why? It is because of the return by value of the getters functions. Returning by value makes a copy that results in poorer performance. Let’s compare the performance between person.getFirstName(); and person.firstName.

Show the performance differences between getters by value and public access.

As you can see, accessing directly the first name instead of a getter is equivalent to a noop.

Getter by const reference

However, it is possible to not return by value but return by reference instead. Going that way, we will have the same performance as without getters. The new code will look like that

class PersonGettersSetters {
  public:
    const std::string &getLastName() const { return m_lastName; }
    const std::string &getFirstName() const { return m_firstName; }
    int getAge() const {return m_age; }
    
    void setLastName(std::string lastName) { m_lastName = std::move(lastName); }
    void setFirstName(std::string firstName) { m_firstName = std::move(firstName); }
    void setAge(int age) {m_age = age; }
  private:
    int m_age = 26;
    std::string m_firstName = "Antoine";
    std::string m_lastName = "MORRIER";    
};

Since we get the same performance as before, are we done? To answer this question, you can try to execute this code.

PersonGettersSetters make() {
    return {};   
}

int main() {
    auto &x = make().getLastName();
     
    std::cout << x << std::endl;
    
    for(auto x : make().getLastName()) {
        std::cout << x << ",";   
    }
}

You may see some weird characters wrote in the console. But why? What happened when you do make().getLastName()?

  1. You create a Person
  2. You get a reference to the last name
  3. You delete Person

You have a dangling reference! It can lead to a crash (in the best case) or something worst than what can be found in a horror movie.

To deal with such a thing, we must introduce reference qualified functions.

class PersonGettersSetters {
  public:
    const std::string &getLastName() const & { return m_lastName; }
    const std::string &getFirstName() const & { return m_firstName; }
    
    std::string getLastName() && { return std::move(m_lastName); }
    std::string getFirstName() && { return std::move(m_firstName); }
    
    int getAge() const {return m_age; }
    
    void setLastName(std::string lastName) { m_lastName = std::move(lastName); }
    void setFirstName(std::string firstName) { m_firstName = std::move(firstName); }
    void setAge(int age) {m_age = age; }
    
  private:
    int m_age = 26;
    std::string m_firstName = "Antoine";
    std::string m_lastName = "MORRIER";    
};

Here is the new solution that works everywhere. You need 2 getters. One for lvalue and one for rvalue (both xvalue and prvalue).

Setter issues

There is not a lot to say in this section. If you want to achieve the best performances, you must write a setter that takes a lvalue and one that takes a rvalue. However, it is generally fine to just have a setter that takes a value that will be moved. Nevertheless, you have to pay the price of an extra move. However, that way, you cannot just make a little enhancement. You must replace the whole variable. If you just wanted to replace one A by a D in a name, it will not be possible by using setters. However, using direct access makes it possible.

What about immutable variables?

Ones will tell you to just make the member attribute as const. However, I am not ok with this solution. Indeed, making it const will prevent the move semantic and will lead to unnecessary copy.

I have no magic solution to propose you right now. Nevertheless, we can write a wrapper which we can named immutable<T>. This wrapper must be able to be :

  1. Constructible
  2. Since it is immutable, it must not be assignable
  3. It can be copy constructible or move constructible
  4. It must be convertible to const T& when being a lvalue
  5. It must be convertible to T when being a rvalue
  6. It must be used like other wrapper through operator* or operator->.
  7. It must be easy to get the address of the underlying object.

Here is a little implementation

#define FWD(x) ::std::forward<decltype(x)>(x)

template <typename T>
struct AsPointer {
    using underlying_type = T;
    AsPointer(T &&v) noexcept : v{std::move(v)} {}
    T &operator*() noexcept { return v; }
    T *operator->() noexcept { return std::addressof(v); }
    T v;
};

template <typename T>
struct AsPointer<T &> {
    using underlying_type = T &;
    AsPointer(T &v) noexcept : v{std::addressof(v)} {}
    T &operator*() noexcept { return *v; }
    T *operator->() noexcept { return v; }
    T *v;
};

template<typename T>
class immutable_t {
  public:
    template <typename _T>
    immutable_t(_T &&t) noexcept : m_object{FWD(t)} {}

    template <typename _T>
    immutable_t &operator=(_T &&) = delete;

    operator const T &() const &noexcept { return m_object; }
    const T &operator*() const &noexcept { return m_object; }
    AsPointer<const T &> operator->() const &noexcept { return m_object; }

    operator T() &&noexcept { return std::move(m_object); }
    T operator*() &&noexcept { return std::move(m_object); }
    AsPointer<T> operator->() &&noexcept { return std::move(m_object); }

    T *operator&() &&noexcept = delete;
    const T *operator&() const &noexcept { return std::addressof(m_object); }

    friend auto operator==(const immutable_t &a, const immutable_t &b) noexcept { return *a == *b; }

    friend auto operator<(const immutable_t &a, const immutable_t &b) noexcept { return *a < *b; }

  private:
    T m_object;
};

So, for an immutable Person, you can just write:

struct ImmutablePerson {
    immutable_t<int> age = 26;
    immutable_t<std::string> firstName = "Antoine";
    immutable_t<std::string> lastName = "MORRIER";
};

Conclusion

I would not say that getters and setters are bad. However, when you do not need to do anything else in your getter and your setter, achieving the best performance, safety and flexibility lead to writing:

  • 3 getters (or even 4): const lvalue, rvalue, const rvalue, and if you want, non-const lvalue (even if sounds really weird since it is easier to just use direct access)
  • 1 setter (or 2 if you want to have the maximum performance)

It is a lot of boilerplate for almost anything.

Some people will tell you that getters and setters enforce encapsulation, but they don’t. Encapsulation is not just making attributes private. It is about hiding things from users, and for structure-like objects, you rarely want to hide anything.

My advice is: when you have a structure like objects, just don’t use getter and setters and go with public / direct access. To be simple, if your setter does not enforce any invariant, you don’t need a private attribute.

PS: For people who use libraries using shallow copy, the performance impact is less important. However, you still need to write 2 functions instead of 0. Don’t forget, the less code you write, the less bugged it will be, easier to maintain, and easier to read.

And you ? Do you use getters and setters? And why?

Comments

12 responses to “Thoughts about getters and setters in C++”

  1. musicaefantasia Avatar

    Nice walk around C++ features, and great analysis: OOP theorists like Allen Hollub already explained that getters and setters are against good OOP practices, because they expose the object members.

  2. atul anand Avatar

    std::string getLastName() && { return std::move(m_lastName); }
    std::string getFirstName() && { return std::move(m_firstName); }

    here you are moving the member variable . so, after the string has been moved we should not access it afterward or else it i will result to undefined behavior, if used as below :-
    PersonGettersSetters obj;
    string szLastName = obj.getLastName();
    cout<<"Last Name = " <<szLastName; // works fine

    szLastName = obj.getLastName(); // Bug : last names is already been moved in previous call.

    1. Antoine MORRIER Avatar
      Antoine MORRIER

      Hello, it is normally not true.
      In your example, obj.getLastName(), the called function will be the const lvalue qualified one, not the rvalue qualified one. So there is no bug in your example.

    2. Antoine MORRIER Avatar
      Antoine MORRIER

      Here, I made a working example to show you that your code is not bugged at all πŸ™‚ https://wandbox.org/permlink/fZFxsKSFA38bKhKW

  3. hi

    I’m really impressed with your writing skills as well as with the layout on your weblog. Is this a paid theme or did you modify it yourself? Either way keep up the excellent quality writing, it is rare to see a nice blog like this one these days.|

    1. Antoine MORRIER Avatar
      Antoine MORRIER

      Thanks for this kind words

      The theme is totally free, I don’t have the skills to modify one by myself

  4. meowsqueak Avatar

    I suppose one argument for using getters/setters functions up front is that you can add extra functionality later without changing the API. E.g. data validation in the setter, or calculating a value or invoking a cache in the getter. If you use public data members, then you’re stuck with them, unless you also have access to all calling code, which often isn’t true.

    That said, I look at all this code to just get/set a person’s name safely and I wonder how we got here. This is atrocious. Our children will not thank us for this abomination we’ve created.

  5. Alex Hajdu Avatar

    I’m using simple rule: If I need setter, I make the variable public. That means, I’m using getters only in situations where I need other objects to acces the varialbe, but don’t want the other objects to modify the value. Modification is done by the class only and other objects are just reading the value.

    1. Antoine MORRIER Avatar
      Antoine MORRIER

      So, are you providing two getters ? One for lvalue and one for rvalue? Would you be interested in using an `immutable` wrapper as I proposed?

      1. Alex Hajdu Avatar

        `immutable` seems like a solution, on the other hand it makes things more complicated. I would say using a simple getter will be never a performance bottle neck for 99,9% applications out there. If there is something special, you usally will be aware of that as this will be different from the things you doing 99% of time anyway; and well, you will write special code to handle the situation. But not needed to do that beforehand. My other rule I like to follow is YAGNI…

        1. Antoine MORRIER Avatar
          Antoine MORRIER

          Seems to be a nice rule πŸ˜‰
          I actually agree with everything you just said, I just wanted to share ideas :).
          It seems we agree with the structure like object πŸ™‚

          1. Alex Hajdu Avatar

            Indeed, I like the article a lot!
            The next is sentences are not related to this article. What I see and I think Bjarne agree as well (at least I’ve got the feeling after looking at his lectures on YT) are over complicated codebases, trying to be super clever and using various language features. Like there are situations we have to use them, but many times are making our lives more complicated and we write dangerous code really easily. The good example is Photoshop. You have a lot of features there, beginners usualy using the army of plugins and effects, but the pros are good to go with some brushes and some basic effects coming with photoshop. Good example is also NASA code base for some of their projects, where things are really super-super simple on purpose…

Leave a Reply