Причины определения неконстантных функций-членов 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()