Эта статья посвящена геттерам и сеттерам в 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()?
Чтобы предупредить это, мы должны ввести 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).
У меня нет волшебного решения, которое я мог бы предложить вам прямо сейчас. Тем не менее, мы можем написать обертку, которую мы можем назвать immutable<T>. Эта обертка должна быть:
#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;
};
Таким образом, для иммутабельного объекта Person вы можете просто написать:
struct ImmutablePerson {
immutable_t<int> age = 26;
immutable_t<std::string> firstName = "Antoine";
immutable_t<std::string> lastName = "MORRIER";
};
Некоторые люди могут вам сказать, что геттеры и сеттеры обеспечивают инкапсуляцию, но это не так. Инкапсуляция - это не просто делать атрибуты приватными. Речь идет о сокрытии внутренностей от пользователей, а в структуроподобных объектах вы редко хотите что-либо скрывать.
Мой совет: когда у перед вами структуроподобный объект, просто не используйте геттеры и сеттеры, а используйте публичный/прямой доступ. Проще говоря, если вам не нужен сеттер для поддержания инвариантности, вам не нужен приватный атрибут.
PS: Для людей, которые используют библиотеки с поверхностным копированием, влияние на производительность менее важно. Однако вам все равно нужно написать 2 функции вместо 0. Не забывайте, что чем меньше кода вы напишете, тем меньше будет ошибок, проще поддерживать и легче читать этот самый код.
Источник статьи: https://habr.com/ru/company/otus/blog/562386/
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<T>. Эта обертка должна быть:
- Constructible
- Так как она immutable, она не должна быть assignable
- Она может быть copy constructible или move constructible
- Она должна быть конвертируемой в const T&, будучи lvalue
- Она должна быть конвертируемой в T, будучи rvalue
- Она должна использоваться, как и другие оболочки, с помощью оператора * или оператора ->.
- Получить адрес базового объекта должно быть легко.
#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;
};
Таким образом, для иммутабельного объекта Person вы можете просто написать:
struct ImmutablePerson {
immutable_t<int> age = 26;
immutable_t<std::string> firstName = "Antoine";
immutable_t<std::string> lastName = "MORRIER";
};
Заключение
Я бы не сказал, что геттеры и сеттеры - это зло. Однако, когда вам не нужно делать что-либо еще в геттере и сеттере, достижение максимальной производительности, безопасности и гибкости подводит вас к написанию:- 3-х геттеров (или даже 4-х): const lvalue, rvalue, const rvalue и, по вашему усмотрению, для неконстантного lvalue (даже если это уже просто очень странно звучит, так как проще использовать прямой доступ)
- 1 сеттер (или 2, если вы хотите выжать максимальную производительность).
Некоторые люди могут вам сказать, что геттеры и сеттеры обеспечивают инкапсуляцию, но это не так. Инкапсуляция - это не просто делать атрибуты приватными. Речь идет о сокрытии внутренностей от пользователей, а в структуроподобных объектах вы редко хотите что-либо скрывать.
Мой совет: когда у перед вами структуроподобный объект, просто не используйте геттеры и сеттеры, а используйте публичный/прямой доступ. Проще говоря, если вам не нужен сеттер для поддержания инвариантности, вам не нужен приватный атрибут.
PS: Для людей, которые используют библиотеки с поверхностным копированием, влияние на производительность менее важно. Однако вам все равно нужно написать 2 функции вместо 0. Не забывайте, что чем меньше кода вы напишете, тем меньше будет ошибок, проще поддерживать и легче читать этот самый код.
Источник статьи: https://habr.com/ru/company/otus/blog/562386/