Причины определения неконстантных функций-членов get?
Я работаю над изучением C++ с помощью книги Страуструпа (Принципы и практика программирования с использованием C++). В упражнении мы определяем простую структуру:
template<typename T>
struct S {
explicit S(T v):val{v} { };
T& get();
const T& get() const;
void set(T v);
void read_val(T& v);
T& operator=(const T& t); // deep copy assignment
private:
T val;
};
Затем нас просят определить const и неконстантную функцию-член для получения val
.
Мне было интересно: есть ли случай, когда имеет смысл иметь неконстантную функцию get
которая возвращает val
?
Мне кажется намного чище, что мы не можем изменить значение в таких ситуациях косвенно. Какие могут быть случаи использования, когда вам нужна const и неконстантная функция get
для возврата переменной-члена?
Ответы
Ответ 1
Неконстантные добытчики?
Получатели и установщики - просто соглашение. Вместо того, чтобы предоставлять метод получения и установки, иногда используется идиома, чтобы обеспечить что-то вроде
struct foo {
int val() const { return val_; }
int& val() { return val_; }
private:
int val_;
};
Так, что в зависимости от константы экземпляра вы получаете ссылку или копию:
void bar(const foo& a, foo& b) {
auto x = a.val(); // calls the const method returning an int
b.val() = x; // calls the non-const method returning an int&
};
Является ли это хорошим стилем в общем, вопрос мнения. Есть случаи, когда это вызывает путаницу, и другие случаи, когда это поведение именно то, что вы ожидаете (см. Ниже).
В любом случае, более важно разработать интерфейс класса в соответствии с тем, что должен делать класс и как вы хотите его использовать, а не слепо следовать соглашениям о методах установки и получения (например, вы должны дать методу осмысленное имя это выражает то, что он делает, а не только с точки зрения "притворяться инкапсулированным и теперь предоставляет мне доступ ко всем вашим внутренним объектам через геттеры", что на самом деле означает использование геттеров везде).
Конкретный пример
Учтите, что доступ к элементам в контейнерах обычно осуществляется следующим образом. В качестве примера игрушки:
struct my_array {
int operator[](unsigned i) const { return data[i]; }
int& operator[](unsigned i) { return data[i]; }
private:
int data[10];
};
Это не работа контейнеров, чтобы скрыть элементы от пользователя (даже data
могут быть открытыми). Вы не хотите, чтобы разные методы обращались к элементам в зависимости от того, хотите ли вы прочитать или записать элемент, поэтому в данном случае вполне целесообразно предоставлять const
и неконстантную перегрузку.
неконстантное получение против инкапсуляции
Может быть, не так очевидно, но это немного спорно ли предоставление добытчиков и сеттеров поддерживает инкапсуляцию или наоборот. Хотя в целом этот вопрос в значительной степени основан на мнениях, для сеттеров и получателей, которые возвращают неконстантные ссылки, речь идет не столько о мнениях. Они нарушают инкапсуляцию. Рассматривать
struct broken {
void set(int x) {
counter++;
val = x;
}
int& get() { return x; }
int get() const { return x; }
private:
int counter = 0;
int value = 0;
};
Этот класс сломан, как следует из названия. Клиенты могут просто получить ссылку, и у класса нет шансов подсчитать, сколько раз значение было изменено (как предполагает set
). После того, как вы вернете неконстантную ссылку, в отношении инкапсуляции будет мало разницы в том, чтобы сделать член открытым. Следовательно, это используется только для случаев, когда такое поведение является естественным (например, контейнер).
PS
Обратите внимание, что ваш пример возвращает const T&
а не значение. Это разумно для шаблонного кода, где вы не знаете, сколько стоит копия, в то время как для int
вы не получите много, возвращая const int&
вместо int
. Для ясности я использовал не шаблонные примеры, хотя для шаблонного кода вы, скорее всего, скорее const T&
.
Ответ 2
Сначала позвольте мне перефразировать ваш вопрос:
Почему неконстантный получатель для члена, а не просто обнародование члена?
Несколько возможных причин причины:
1. Легко для инструмента
Кто сказал, что неконстантный получатель должен быть просто:
T& get() { return val; }
? это может быть что-то вроде:
T& get() {
if (check_for_something_bad()) {
throw std::runtime_error{"Attempt to mutate val when bad things have happened");
}
return val;
}
However, as @BenVoigt suggests, it is more appropriate to wait until the caller actually tries to mutate the value through the reference before spewing an error.
2. Культурная конвенция/"так сказал начальник"
Некоторые организации применяют стандарты кодирования. Эти стандарты кодирования иногда создаются людьми, которые могут быть чрезмерно оборонительными. Итак, вы можете увидеть что-то вроде:
Если ваш класс не является "простым старым типом данных", никакие члены данных не могут быть открытыми. Вы можете использовать методы получения для таких непубличных участников по мере необходимости.
и тогда, даже если для определенного класса имеет смысл просто разрешить неконстантный доступ, этого не произойдет.
3. Может, val
просто нет?
Вы привели пример, в котором val
действительно существует в экземпляре класса. Но на самом деле - это не обязательно! Метод get()
может возвращать некоторый тип прокси-объекта, который при назначении, мутации и т.д. Выполняет некоторые вычисления (например, сохраняет или извлекает данные в базе данных).
4. Позволяет изменять внутренние компоненты класса позже без изменения кода пользователя.
Теперь, читая пункты 1 или 3 выше, вы можете спросить: "но моя struct S
имеет val
!" или "мой get()
не делает ничего интересного!" - ну, правда, нет; но вы можете изменить это поведение в будущем. Без get()
все пользователи вашего класса должны будут изменить свой код. С помощью get()
вам нужно всего лишь внести изменения в реализацию struct S
Я не сторонник такого подхода к проектированию, но некоторые программисты это делают.
Ответ 3
get()
вызывается неконстантными объектами, которым разрешено изменяться, вы можете сделать:
S r(0);
r.get() = 1;
но если вы сделаете r
const как const S r(0)
, строка r.get() = 1
больше не будет компилироваться, даже для получения значения, поэтому вам нужна const-версия const T& get() const
по крайней мере, для чтобы получить значение для константных объектов, это позволяет вам:
const S r(0)
int val = r.get()
Константная версия функций-членов пытается соответствовать свойству constness объекта, на котором сделан вызов, то есть если объект является неизменяемым, будучи const, и функция-член возвращает ссылку, она может отражать константность вызывающей стороны, возвращая константная ссылка, таким образом сохраняя свойство неизменности объекта.
Ответ 4
Это зависит от цели S
Если это какая-то тонкая оболочка, может быть целесообразно разрешить пользователю прямой доступ к значению подложки.
Одним из реальных примеров является std::reference_wrapper
.
Ответ 5
Нет. Если получатель просто возвращает неконстантную ссылку на член, например:
private:
Object m_member;
public:
Object &getMember() {
return m_member;
}
Тогда m_member
должно быть общедоступным, а m_member
доступа не требуется. Нет абсолютно никакого смысла делать этот элемент приватным, а затем создавать метод доступа, который дает всем доступ к нему.
Если вы вызываете getMember()
, вы можете сохранить результирующую ссылку на указатель/ссылку, а после этого вы можете делать с m_member
все, что захотите, включающий класс ничего об этом не узнает. Это так же, как если бы m_member
был публичным.
Обратите внимание, что если getMember()
выполняет какую-то дополнительную задачу (например, он не просто возвращает m_member
, но лениво m_member
его), тогда getMember()
может быть полезен:
Object &getMember() {
if (!m_member) m_member = new Object;
return *m_member;
}
Ответ 6
Я также хотел бы указать на практическую сторону вопроса. Например, вы хотите объединить ваш объект Bar
в другой объект Foo
и создать постоянный метод doSomething
который использует Bar
только для чтения:
class Foo
{
Bar bar_;
public:
void doSomething() const
{
//bar_.get();
}
}
У тебя проблемы! Вы абсолютно уверены, что не изменяете объект Foo
, вы хотите пометить это модификатором const
, но вы не можете этого сделать, потому что get
не является const
. Другими словами, вы создаете свои собственные проблемы на будущее и усложняете написание четкого и качественного кода. Но вы можете реализовать две версии для get
если вам это действительно нужно:
class Bar
{
int data = 1;
public:
int& get() { return data; };
const int& get() const { return data; };
};
такая ситуация может возникнуть, если у вас уже есть код, в котором постоянные модификаторы игнорируются, поэтому такая ситуация является обычной:
foo(int* a); // Just get a, don't modified
foo(&bar_.get()); // need non constant get()