В чём разница между геттерами? И какой вариант лучше?
Я новичок в с++, и увидел много разных вариантов геттеров, но не понимаю в чём разница и какой вариант лучше.
Вариант №1
T getValue()
Вариант №2
T getValue() const
Вариант №3
const T &getValue() const
Вариант №4
const T &getValue() const &
Вариант №5
T *getValue()
Ответы (2 шт):
Такое лучше разделить на куски, тогда будет легче заметно.
Вначале рассмотрим возвращаемое значение. У Вас есть по значению (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();// &
Простой ответ: используйте 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));.