A couple of thoughts on getters and setters in C ++

This article is about getters and setters in C ++. I apologize, but this is not about coroutines. By the way, the second part about thread pools will appear in the near future.

TL; DR: getters and setters are not very well suited for structured objects.

Introduction

In this article, I only express my personal opinion, I do not pursue the goal of offending or offending anyone, I am just going to explain why and when it is worth or not to use getters and setters. I would be very glad to have any discussions in the comments.

It should be immediately clear that when I talk about a getter I mean a function that just returns something, and when I talk about a setter I mean a function that just changes one internal value without performing any checks or other additional calculations.

Performance and getters

Let’s say we have a simple structure with regular getters 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 version without getters and setters.

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

It is much more concise and reliable. Here we cannot, for example, return the last name instead of the first name.

Both codes are fully functional. We have a Person class named (firstName), last name (lastName) and age (age). However, suppose we want a function that returns some kind of summary for a specific 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);
}

The version without getters accomplishes this task 30% faster than the version with getters. Why? Because of the return by value in the getter. Returning by value creates a copy, which degrades performance. Let’s compare the performance person.getFirstName(); and person.firstName

As you can see, directly accessing the name field without a getter is equivalent to noop.

Getter by constant reference

However, you can use return not by value, but by reference. Thus, we get the same performance as without using getters. The updated code will look like this:

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 in the concise version, we can calm down on that, right? Before answering this question, please try 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 notice some strange characters displayed in the console. But why? What happened when we did make().getLastName()?

  1. You are creating an instance of Person.

  2. You will receive a link to the last name.

  3. You are deleting the Person instance.

And here we have a dangling link! It can lead to crashes (at best) or something even worse, something that can only be found in horror movies.

To prevent this, we must enter ref-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’s a new solution that will work everywhere. You need two getters. One for lvalue and one for rvalue (as xvalueand for prvalue).

Setter issues

There isn’t much to say here. If you want the best performance you should write a single setter that takes lvalue, and one that takes rvalue… However, as a rule, it is sufficient to have only one setter that accepts the value to be moved. However, you will have to pay for this with an additional move. However, this way you won’t be able to make small changes to the variables. You must replace the entire variable. If you just want to replace one letter A in the name with D, then you cannot do that with setters. However, you can do this with direct access.

What about immutable variables?

Someone might advise you to just make the member attribute const. However, this solution does not suit me. Creating a constant will prevent move semantics and lead to unnecessary copying.

I have no magic solution that I can offer you right now. However, we can write a wrapper that we can call immutable. This wrapper should be:

  1. Constructible

  2. Because she is immutable, it shouldn’t be assignable

  3. She could be copy constructible or move constructible

  4. It must be convertible to const T& being lvalue

  5. It must be convertible to Tbeing rvalue

  6. It should be used like other shells using the operator * or operator ->

  7. It should be easy to get the address of the base object.

Here’s a small 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

    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 object, you can simply 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 evil. However, when you don’t need to do anything else in the getter and setter, achieving maximum performance, security, and flexibility leads you to write:

  • 3 getters (or even 4): const lvalue, rvalue, const rvalue and, at your discretion, for non-const lvalue (even if it just sounds very strange, since it’s easier to use direct access)

  • 1 setter (or 2 if you want to squeeze out maximum performance).

This is by and large a template that works for almost anything.

Some people may tell you that getters and setters provide encapsulation, but they are not. Encapsulation isn’t just about making attributes private. It’s about hiding internals from users, and in structured objects, you rarely want to hide anything.

My advice: when you have a structured object in front of you, just don’t use getters and setters, but use public / direct access. Simply put, if you don’t need a setter to maintain invariance, you don’t need a private attribute.

PS: For people who use shallow copy libraries, the performance impact is less important. However, you still need to write 2 functions instead of 0. Do not forget that the less code you write, the fewer errors you will have, the easier it is to maintain, and the easier to read this very code.

Well, what do you think? Are you using getters and setters? And why?


The translation of the material was prepared as part of the course “C ++ Developer. Basic”… We invite everyone to a two-day online intensive “HTTPS and threads in C ++. From simple to beautiful “… On the first day of the intensive we will set up our http-server and analyze it what is called “from and to”. On the second day, we will make all the necessary measurements and make our server super fast, which will help us understand with an example why the C ++ language is better than others. check in here

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *