Семантика ссылки константы C++?

Рассмотрите заявление приложения ниже. Это демонстрирует то, что я назвал бы дефектным дизайном класса.

#include <iostream>

using namespace std;

struct B
{
 B() : m_value(1) {}

 long m_value;
};

struct A
{
 const B& GetB() const { return m_B; }

 void Foo(const B &b)
 {
  // assert(this != &b);
  m_B.m_value += b.m_value;
  m_B.m_value += b.m_value;
 }

protected:
 B m_B;
};

int main(int argc, char* argv[])
{
 A a;

 cout << "Original value: " << a.GetB().m_value << endl;

 cout << "Expected value: 3" << endl;
 a.Foo(a.GetB());

 cout << "Actual value: " << a.GetB().m_value << endl;

 return 0;
}

Вывод:
Исходное значение: 1
Ожидаемое значение: 3
Фактическое значение: 4

Очевидно, программиста дурачит constness b. По ошибке b точки к this, который приводит к нежелательному поведению.

Мой вопрос: За какими правилами константы необходимо следовать при разработке методов считывания/методов set?

Мое предложение: Никогда не возвращайте ссылку на членскую переменную, если она может быть установлена ссылкой через функцию членства. Следовательно, или возврат значением или параметры передачи значением. (Современные компиляторы оптимизируют далеко дополнительную копию так или иначе.)

6
задан l33t 11 June 2010 в 11:11
поделиться

5 ответов

Очевидно, что программист одурачен постоянством b

Как кто-то однажды сказал, Вы продолжаете использовать это слово. Я не думаю, что оно означает то, что вы думаете, что оно означает.

Const означает, что вы не можете изменить значение. Это не значит, что значение не может измениться.

Если программиста обманывает тот факт, что какой-то другой код может изменить то, что он не может, ему нужно лучше изучить aliasing.

Если программиста обманывает тот факт, что лексема 'const' звучит немного похоже на 'constant', но означает 'только для чтения', ему нужно лучше изучить семантику языка программирования, который он использует.

Итак, если у вас есть геттер, который возвращает const-ссылку, то это псевдоним для объекта, который вы не имеете права изменять. Это не говорит ничего о том, является ли его значение неизменяемым.


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

Если вы сделаете B.m_value приватным, то вы не сможете написать Foo, который у вас есть. Вы либо делаете Foo в:

void Foo(const B &b)
{
    m_B.increment_by(b);
    m_B.increment_by(b);
}

void B::increment_by (const B& b)
{
    // assert ( this != &b ) if you like 
    m_value += b.m_value;
}

либо, если вы хотите, чтобы значение было постоянным, используете временное

void Foo(B b)
{
    m_B.increment_by(b);
    m_B.increment_by(b);
}

Теперь, увеличение значения само по себе может быть или не быть разумным, и это легко проверяется в B::increment_by. Вы также можете проверить, является ли &m_b==&b в A::Foo, хотя как только у вас появится несколько уровней объектов и объектов со ссылками на другие объекты, а не на значения (так &a1.b.c == &a2. b.c не означает, что &a1.b==&a2.b или &a1==&a2), тогда вы действительно должны просто знать, что любая операция потенциально алиасирована.

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

Передача аргументов, имеющих наименьшую структуру, также хорошо работает. Если бы Foo() принимала long, а не объект, из которого она должна получить long, то она не страдала бы от алиасинга, и вам не нужно было бы писать другую Foo(), чтобы увеличить m_b на значение C.

.
20
ответ дан 8 December 2019 в 05:20
поделиться

Я предлагаю несколько иное решение, которое имеет ряд преимуществ (особенно в постоянно растущем многопоточном мире). Идея проста и заключается в том, чтобы "фиксировать" свои изменения последними.

Чтобы объяснить на вашем примере, вы просто измените класс 'A' на:

struct A
{
 const B& GetB() const { return m_B; }

 void Foo(const B &b)
 {
  // copy out what we are going to change;
  int itm_value = m_b.m_value;

  // perform operations on the copy, not our internal value
  itm_value += b.m_value;
  itm_value += b.m_value;

  // copy over final results
  m_B.m_value = itm_value ;
 }

protected:
 B m_B;
};

Идея здесь в том, чтобы поместить все присваивания в память, доступные для просмотра, над текущей функцией в конце, где они практически не могут потерпеть неудачу. Таким образом, если в середине операции возникнет ошибка (скажем, в середине этих двух операций произойдет деление, и если оно окажется равным 0), то мы не останемся с половинчатыми данными в середине.

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

Кроме того, компилятор обычно может лучше оптимизировать эту операцию, поскольку больше не требуется записывать изменяемые переменные в память (мы заставляем только одно чтение изменяемого значения и одну запись). Таким образом, у компилятора есть возможность просто хранить соответствующие данные в регистре, экономя доступ к кэшу L1, если не промахи кэша. В противном случае компилятор, вероятно, заставит его записывать в память, так как он не знает, какое алиасинг может иметь место (поэтому он не может гарантировать, что эти значения остаются неизменными, если они все локальные, он знает, что это не может быть алиасинг, потому что текущая функция - единственная, которая знает об этом).

Есть много разных вещей, которые могут произойти с исходным кодом. Я не удивлюсь, если некоторые компиляторы (с включенными оптимизациями) действительно произведут код, который даст "ожидаемый" результат, в то время как другие - нет. Все это просто потому, что момент, когда переменные, которые не являются "летучими", действительно записываются/читаются из памяти, не очень хорошо определен в стандартах c++.

2
ответ дан 8 December 2019 в 05:20
поделиться

Настоящей проблемой здесь является атомарность. Условием функции Foo является то, что ее аргумент не меняется во время использования.

Если бы, например, Foo была указана с аргументом value-argument, т.е. со ссылочным аргументом, проблемы бы не возникло.

1
ответ дан 8 December 2019 в 05:20
поделиться

Честно говоря, A::Foo() меня смущает больше, чем ваша исходная проблема. Как бы я на это ни смотрел, это должно быть B::Foo(). И внутри B::Foo() проверка на this не была бы такой уж необычной.

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

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

Если у вас постоянно возникают такие проблемы, я бы сказал, что более простым правилом будет всегда возвращать копию объекта вместо ссылки. Потому что довольно часто, если объект большой, то вам все равно придется обрабатывать его иначе, чем остальные.

1
ответ дан 8 December 2019 в 05:20
поделиться

Мой глупый ответ, я оставляю его здесь на всякий случай, если кто-то другой придет к такой же плохой идее:

Проблема в том, что я думаю, что упомянутый объект не является const ( B const & vs const B & ), только ссылка в вашем коде является const.

-1
ответ дан 8 December 2019 в 05:20
поделиться
Другие вопросы по тегам:

Похожие вопросы: