В чём разница между геттерами? И какой вариант лучше?

Я новичок в с++, и увидел много разных вариантов геттеров, но не понимаю в чём разница и какой вариант лучше.

Вариант №1 T getValue()

Вариант №2 T getValue() const

Вариант №3 const T &getValue() const

Вариант №4 const T &getValue() const &

Вариант №5 T *getValue()


Ответы (2 шт):

Автор решения: KoVadim

Такое лучше разделить на куски, тогда будет легче заметно.

Вначале рассмотрим возвращаемое значение. У Вас есть по значению (1 и 2), по ссылке на константу (3 и 4) и по указателю.

По значению хорошо возвращать небольшие типы. Например int или double. (есть мнение, что небольшой это до 8-12 байт, в зависимости от архитектуры). А вот что то большое так лучше не возвращать - лишняя копия это всегда лишняя копия, процесс копирования может занимать много времени (представьте себе вектор на миллионы элементов). А некоторые вещи (например, std::fstream) так просто и не скопируешь.

Но если Ваш геттер просто генерирует данные, например так

std::string get(size_t n) {
  return std::string(n, '*');
}

то это нормально. Современные компиляторы дают гарантированный copy elision (раньше называлось RVO) и убирают промежуточные вызовы конструкторов-деструкторов.

Если же хочется возвратить что то большое, что и так хранится внутри объекта - ссылка на константу обычная практика (некоторые ошибочно называют по константной ссылке, но это не так:) ). Скажем так, если объект большой - делайте, скорее всего это будет самым оптимальным вариантом.

А вот возвращать по указателю - это очень плохой вариант. Во первых, вызывающая сторона будет проверять на nullptr, что бы не получить проблем (а оно может и не нужно), во-вторых, не понятно, а должна ли вызывающая сторона как то почистить полученный указатель (вызвать free/delete), в третьих, по указателю объект легко модифицировать и вызвать кучу проблем.

Теперь рассмотрим const после скобок. Здесь он относится к this. И утверждает, что Ваша функция не изменяет объект. Это хорошая гарантия и ее любят. А также не стоит забывать о том, что const любит "заражать все", и иногда это может привести к тому, что придется везде расставлять const (хотя некоторые это очень любят), а потом ещё и mutable. Поэтому, если Вы просто что то мелкое возвращаете, то это хорошее слово и обычно его лучше добавить.

Теперь рассмотрим третий кусочек - амперсанд в конце. На самом деле там может быть и два амперсанда. Это интересный прием и позволяет делать перегрузку (а можно и разрешить/запретить) вызывать функцию в определенных случаях). Один амперсанд разрешает вызывать от lvalue, а двойной от rvalue. Самый простой случай, когда это имеет значение, следующий (детали на хабре - https://habr.com/ru/post/487920/ )

class Foo{ void test()& {std::cout << "&";} void test()&& {std::cout << "&&";} }
Foo().test(); // && 
Foo f;
f.test();// &
→ Ссылка
Автор решения: HolyBlackCat

Простой ответ: используйте const T &getValue() const и не парьтесь. Если T дешево копировать, то можете возвращать по значению.


(1) плох тем, что его нельзя вызвать на константном объекте.

(1) и (2) копируют возвращаемое значение. Это плохо, если T дорого копировать. Если вы уверены, что T очень дешево копировать (например, T = int), то этот вариант подходит.

Еще, это позволяет не хранить результат в классе, и вычислять его на лету. Даже если этого не происходит сейчас, можно оставить задел на будущее.

(3) - правильный вариант, используйте его по умолчанию.

(4) сам по себе ничем не отличается от (3), поэтому лишний амперсанд лучше не писать.

Но (4) имеет смысл, если вы хотите добавить к нему в пару геттер в духе T &&getValue() &&. (Попытка дописать его в пару к (3) вместо (4) вызовет ошибку компиляции.)

T &&getValue() && будет вызван вместо (4), если позвать геттер на временном объекте, и за счет && в возвращаемом типе, результат будет автоматически перемещен, без необходимости писать std::move.

В теории, такая пара геттеров лучше, чем (3), но на практике - лениво писать по два геттера на одно поле.

Поэтому обычно так делают в особых случаях, когда это автоматическое перемещение действительно важно. Например, std::optional::operator* (там вообще сделали 4 перегрузки вместо 2), или std::stringstream::str() (там обе перегрузки возвращают по значению, видимо чтобы не обязывать хранить T внутри класса).

(5) - сеттер, а не геттер.

Обычно возвращают по ссылке. Указатель имеет смысл, если в каких-то ситуациях нужно возвращать ноль.

К сеттеру в пару нужен геттер, (3).

Обычно делают не так, а передают новое значение сеттеру в параметр: void setValue(T value) {... = std::move(value);}.

Смысл сеттеров в том, чтобы иметь возможность выполнять какие-то действия одновременно с присваиванием (или вместо него). Поэтому сеттер, возвращающий по ссылке, намного менее гибок - вы не можете остановить присваивание, и не можете выполнять никаких действий после него (только до), и не можете узнать новое значение поля.

Если вы вообще не хотите делать ничего одновременно с присваиванием, возможно стоит сделать поле публичным.

Но если вам нужно делать что-то перед присваиванием, и вы полностью уверены, что этого вам хватит, то возвращать по ссылке удобнее. Сравните x.setValue().push_back(42); и auto y = x.getValue(); x.push_back(42); x.setValue(std::move(y));.

→ Ссылка