[Перевод] Пара мыслей о геттерах и сеттерах в C++

Эта статья посвящена геттерам и сеттерам в C++. Приношу свои извинения, но речь пойдет не о корутинах. К слову, в ближайшее время появится вторая часть про пулы потоков.
TL; DR: геттеры и сеттеры не очень хорошо подходят для структуроподобных объектов.
Введение
В этой статье я лишь высказываю свое личное мнение, я не преследую цели кого-нибудь обидеть или задеть, я просто собираюсь объяснить, почему и когда стоит или не стоит, использовать геттеры и сеттеры. Буду очень рад любым дискуссиям в комментариях.
Следует сразу прояснить, что когда я говорю о геттере, я подразумеваю функцию, которая просто что-то возвращает, а когда я говорю о сеттере, я подразумеваю функцию, которая просто изменяет одно внутреннее значение, не выполняя никаких проверок или других дополнительных вычислений.
Производительность и геттеры
Допустим, у нас есть простая структура с обычными геттерами и сеттерами:
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";
};Сравним эту версию с версией без геттеров и сеттеров.
struct Person {
int age = 26;
std::string firstName = "Antoine";
std::string lastName = "MORRIER";
};Она намного лаконичнее и надежнее. Здесь мы не можем, например, верну фамилию вместо имени.
Оба кода полностью функциональны. У нас есть класс Person с именем (firstName), фамилией (lastName) и возрастом (age). Однако предположим, что нам нужна функция, которая возвращает некоторую сводку по конкретному человеку.
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);
}
Версия без геттеров выполняет эту задачу на 30% быстрее, чем версия с геттерами. Почему? Из-за возврата по значению в геттере. При возврате по значению создается копия, что снижает производительность. Давайте сравним производительность person.getFirstName(); и person.firstName.

Как видите, прямой доступ к полю имени без геттера эквивалентен noop.
Геттер по константной ссылке
Однако можно использовать возврат не по значению, а по ссылке. Таким образом мы получим такую же производительность, как и без использования геттеров. Обновленный код будет выглядеть так:
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";
};
Так как мы получаем ту же производительность, что и в лаконичной версии, мы можем на этом успокоиться, не так ли? Прежде чем отвечать на этот вопрос, попробуйте выполнить этот код.
PersonGettersSetters make() {
return {};
}
int main() {
auto &x = make().getLastName();
std::cout << x << std::endl;
for(auto x : make().getLastName()) {
std::cout << x << ",";
}
}Вы можете заметить некоторые странные символы, выведенные в консоли. Но почему? Что произошло, когда мы сделали make().getLastName()?
Вы создаете экземпляр Person.
Вы получаете ссылку на фамилию.
Вы удаляете экземпляр Person.
И вот у нас есть висячая ссылка! Это может привести к крашам (в лучшем случае) или чему-то еще более худшему, чему-то, что можно найти только в фильмах ужасов.
Чтобы предупредить это, мы должны ввести ref-qualified функции.
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";
};Вот новое решение, которое будет работать везде. Вам нужно два геттера. Один для lvalue и один для rvalue (как xvalue, так и для prvalue).
Проблемы с сеттерами
Тут особо нечего сказать. Если вы хотите добиться максимальной производительности, вы должны написать один сеттер, который принимает lvalue, и один, который принимает rvalue. Однако, как правило, достаточно иметь всего один сеттер, который принимает перемещаемое значение. Тем не менее, вам придется расплатиться за это дополнительным move. Однако таким образом у вас не получится производить небольшие изменения в переменных. Вы должны заменять всю переменную целиком. Если вы просто хотите заменить одну букву A в имени на D, то вы не сможете сделать это с помощью сеттеров. Однако с помощью прямого доступа так делать можно.
А как насчет иммутабельных переменных?
Кто-то может посоветовать вам просто сделать атрибут члена const. Однако меня это решение не устраивает. Создание константы предотвратит move-семантику и приведет к ненужному копированию.
У меня нет волшебного решения, которое я мог бы предложить вам прямо сейчас. Тем не менее, мы можем написать обертку, которую мы можем назвать immutable
ConstructibleТак как она
immutable, она не должна бытьassignableОна может быть copy
constructibleилиmove constructibleОна должна быть конвертируемой в
const T&, будучиlvalueОна должна быть конвертируемой в
T, будучиrvalueОна должна использоваться, как и другие оболочки, с помощью оператора
*или оператора->.Получить адрес базового объекта должно быть легко.
Вот небольшая реализация:
#define FWD(x) ::std::forward(x)
template
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
struct AsPointer {
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
class immutable_t {
public:
template
immutable_t(_T &&t) noexcept : m_object{FWD(t)} {}
template
immutable_t &operator=(_T &&) = delete;
operator const T &() const &noexcept { return m_object; }
const T &operator*() const &noexcept { return m_object; }
AsPointer operator->() const &noexcept { return m_object; }
operator T() &&noexcept { return std::move(m_object); }
T operator*() &&noexcept { return std::move(m_object); }
AsPointer 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;
}; Таким образом, для иммутабельного объекта Person вы можете просто написать:
struct ImmutablePerson {
immutable_t age = 26;
immutable_t firstName = "Antoine";
immutable_t lastName = "MORRIER";
}; Заключение
Я бы не сказал, что геттеры и сеттеры — это зло. Однако, когда вам не нужно делать что-либо еще в геттере и сеттере, достижение максимальной производительности, безопасности и гибкости подводит вас к написанию:
3-х геттеров (или даже 4-х):
const lvalue,rvalue,const rvalueи, по вашему усмотрению, для неконстантногоlvalue(даже если это уже просто очень странно звучит, так как проще использовать прямой доступ)
1 сеттер (или 2, если вы хотите выжать максимальную производительность).
Это по большому счету шаблон, который подходит практически для всего.
Некоторые люди могут вам сказать, что геттеры и сеттеры обеспечивают инкапсуляцию, но это не так. Инкапсуляция — это не просто делать атрибуты приватными. Речь идет о сокрытии внутренностей от пользователей, а в структуроподобных объектах вы редко хотите что-либо скрывать.
Мой совет: когда у перед вами структуроподобный объект, просто не используйте геттеры и сеттеры, а используйте публичный/прямой доступ. Проще говоря, если вам не нужен сеттер для поддержания инвариантности, вам не нужен приватный атрибут.
PS: Для людей, которые используют библиотеки с поверхностным копированием, влияние на производительность менее важно. Однако вам все равно нужно написать 2 функции вместо 0. Не забывайте, что чем меньше кода вы напишете, тем меньше будет ошибок, проще поддерживать и легче читать этот самый код.
Ну, а что думаете вы? Используете ли вы геттеры и сеттеры? И почему?
Перевод материала подготовлен в рамках курса «C++ Developer. Basic». Всех желающих приглашаем на двухдневный онлайн-интенсив «HTTPS и треды в С++. От простого к прекрасному». В первый день интенсива мы настроим свой http-сервер и разберем его что называется «от и до». Во второй день произведем все необходимые замеры и сделаем наш сервер супер быстрым, что поможет нам понять на примере, чем же все-таки язык С++ лучше других. Регистрация здесь
