Приблизительно шесть лет назад разработчик программного обеспечения по имени Harri Porten написал эту статью, задав вопрос, "Когда функция членства должна иметь спецификатор константы и когда не был должен он?" Я нашел, что это было лучшей рецензией, которую я мог найти проблемы, с которой я боролся позже и которая я думаю, не хорошо покрыт большинством обсуждений, которые я нашел на правильности константы. Так как сайт совместного пользования информацией программного обеспечения, столь же мощный как ТАК, не существовал тогда, я хотел бы возродить вопрос здесь.
Статья, кажется, охватывает много основных вопросов, но у автора все еще есть вопрос о const и nonconst перегрузках функций, возвращающих указатели. Последняя строка статьи:
Многие, вероятно, ответят: "Зависит.", но я бы хотел спросить: "Зависит от чего?"
Если быть абсолютно точным, то зависит от того, является ли состояние указателя объекта A логически частью состояния этого
объекта.
Для примера, где это так, vector
возвращает ссылку на int. Ссылка на int является "частью" вектора, хотя на самом деле она не является членом данных. Таким образом, применяется идиома const-overload: измените элемент, и вы измените вектор.
Для примера, где это не так, рассмотрим shared_ptr
. У него есть функция-член T * operator->() const;
, потому что имеет логический смысл иметь const умный указатель на неconst объект. Ссылка не является частью умного указателя: ее модификация не изменяет умный указатель. Поэтому вопрос о том, можно ли "пересадить" умный указатель, чтобы он ссылался на другой объект, не зависит от того, является ли referand const.
Я не думаю, что могу предоставить какие-либо полные рекомендации, которые позволят вам решить, является ли указатель логически частью объекта или нет. Однако, если модификация указателя изменяет возвращаемые значения или другое поведение любых функций-членов this
, и особенно если указатель участвует в operator==
, то есть шанс, что он логически является частью объекта this
.
Я бы предпочел считать, что он является частью (и обеспечить перегрузки). Затем, если возникнет ситуация, когда компилятор пожалуется, что я пытаюсь изменить объект A, возвращаемый из const-объекта, я подумаю, действительно ли я должен это делать или нет, и если да, то изменю дизайн так, чтобы только указатель на-A концептуально был частью состояния объекта, а не сам A. Для этого, конечно, необходимо убедиться, что изменение A не делает ничего, что нарушает ожидаемое поведение этого
const-объекта.
Если вы публикуете интерфейс, вам, возможно, придется выяснить это заранее, но на практике возврат от перегрузок const к const-функции, возвращающей не-const-указатель, вряд ли нарушит клиентский код. В любом случае, к моменту публикации интерфейса вы, надеюсь, уже немного попользовались им и, возможно, получили представление о том, что на самом деле включает в себя состояние вашего объекта.
Кстати, я также стараюсь не предоставлять аксессоров указателей/ссылок, особенно модифицируемых. Это действительно отдельный вопрос (закон Деметры и все такое), но чем больше раз вы можете заменить:
A *getA();
const A *getA() const;
на:
A getA() const; // or const A &getA() const; to avoid a copy
void setA(const A &a);
тем меньше вам придется беспокоиться об этом. Конечно, у последнего есть свои ограничения.
, когда он не изменяет объект.
Он просто заставляет this
иметь тип const myclass *
. Это гарантирует вызывающей функции, что объект не изменится. Допускается некоторая оптимизация компилятора, и программисту легче узнать, может ли он вызвать его без побочных эффектов (по крайней мере, эффектов для объекта).
Общее правило:
Функция-член должна быть
const
, если она компилируется, когда помечена какconst
и , если она все равно будет компилироваться, еслиconst
являются транзитивными указателями w.r.t.
Исключения:
const
Одно интересное практическое правило, которое я обнаружил во время исследования, взято из здесь :
Хорошее практическое правило для LogicalConst следующее: если операция сохраняет LogicalConstness, то если старое состояние и новое состояние сравниваются с EqualityOperator, результат должен быть истинным. Другими словами, EqualityOperator должен отражать логическое состояние объекта.
Я лично использую очень простое практическое правило:
Если наблюдаемое состояние объекта не изменяется при вызове данного метода, этот метод должен быть const
.
В целом это похоже на правило, упомянутое SCFrench
о сравнении равенства, за исключением того, что большинство моих классов нельзя сравнивать.
Я хотел бы продвинуть обсуждение еще на один шаг:
Когда требуется аргумент, функция должна принимать его с помощью дескриптора const
(или копировать), если аргумент остается неизменным (для внешний наблюдатель)
Это немного более общий характер, поскольку в конце концов метод класса - это не что иное, как автономная функция, принимающая экземпляр класса в качестве первого аргумента:
class Foo { void bar() const; };
эквивалентно:
class Foo { friend void bar(const Foo& self); }; // ALA Python
Вот несколько хороших статей:
Herb Sutter's GotW # 6
Herb Sutter & const для оптимизации
Дополнительные советы по корректности const
Из Википедии
Я использую квалификаторы метода const
, когда метод не изменяет элементы данных класса или его общая цель - не изменять элементы данных. Один из примеров включает RAII для метода получателя , который может инициализировать элементы данных (например, извлекать из базы данных). В этом примере метод изменяет член (ы) данных только один раз во время инициализации; во всех остальных случаях - постоянно.
Я разрешаю компилятору перехватывать ошибки const во время компиляции, а не я их перехватываю во время выполнения (или пользователя).