Правильное использование const // FAQ C++

Добавлено 22 сентября 2020 в 20:22

Что такое «правильное использование const» (const correctness)?

Хорошая вещь. Это означает использование ключевого слова const для предотвращения изменения константных объектов.

Например, если вы хотите создать функцию f(), которая принимает std::string, плюс вы хотите пообещать вызывающим не изменять std::string, которая передается в f(), вы можете заставить f() получать ее параметр std::string как…

  • void f1(const std::string& s); // передача по ссылке на константу
  • void f2(const std::string* sptr); // передача по указателю на константу
  • void f3(std::string s); // передача по значению

В случаях передачи по ссылке на константу и передачи по указателю на константу любые попытки изменить std::string вызывающего объекта в функциях f() будут отмечены компилятором как ошибка времени компиляции. Эта проверка выполняется полностью во время компиляции: для const не требуется места или скорости во время выполнения. В случае передачи по значению (f3()) вызываемая функция получает копию std::string вызывающего объекта. Это означает, что f3() может изменить свою локальную копию, но эта копия уничтожается, когда происходит возврат из f3(). То есть f3() не может изменить объект std::string вызывающего объекта.

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

  • void g1(std::string& s); // передача по ссылке на неконстанту
  • void g2(std::string* sptr); // передача по указателю на неконстанту

Отсутствие const в этих функциях сообщает компилятору, что им разрешено (но не обязательно) изменять объект std::string вызывающего объекта. Таким образом, они могут передавать свой объект std::string в любую из функций f(), но только f3() (которая получает свой параметр «по значению») может передать свой объект std::string в g1() или g2(). Если в f1() или f2() необходимо вызвать функцию g(), в функцию g() должна быть передана локальная копия объекта std::string; параметр для f1() или f2() не может быть напрямую передан ни в одну из функций g(). Например,

void g1(std::string& s);

void f1(const std::string& s)
{
  g1(s);          // Ошибка компиляции, поскольку s является константой
  std::string localCopy = s;
  g1(localCopy);  // Хорошо, поскольку localCopy не является константой
}

Естественно, что в приведенном выше случае любые изменения, которые делает g1(), вносятся в объект localCopy, который является локальным для f1(). То есть в константный параметр, переданный по ссылке в f1(), не будут внесены никакие изменения.


Как «правильное использование const» связано с безопасностью обычных типов?

Объявление константности параметра – это просто еще одна форма безопасности типов.

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

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

Концептуально вы можете представить, что, например, const std::string – это другой класс, отличающийся от обычного std::string, поскольку в варианте с const концептуально отсутствуют различные модифицирующие операции, доступные в варианте без const. Например, вы можете концептуально представить, что const std::string просто не имеет оператора присваивания += или каких-либо других изменяющих операций.


Когда я должен попытаться исправить ситуацию с const, «раньше» или «позже»?

В самом-самом начале.

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

Добавляйте const рано и часто.


Что означает "const X* p"?

Это означает, что p указывает на объект класса X, но p не может использоваться для изменения этого объекта X (естественно, p также может быть NULL).

Читайте справа налево: «p – указатель на константный X».

Например, если у класса X есть константная функция-член, такая как inspect() const, можно сказать p->inspect(). Но если у класса X есть неконстантная функция-член с именем mutate(), то p->mutate() будет ошибкой.

Важно отметить, что эта ошибка обнаруживается компилятором во время компиляции – проверки во время выполнения не выполняются. Это означает, что const не замедляет вашу программу и не требует от вас написания дополнительных тестовых случаев для проверки во время выполнения – компилятор выполняет эту работу во время компиляции.


В чем разница между "const X* p", "X* const p" и "const X* const p"?

Читайте объявления указателя справа налево.

  • const X* p означает «p указывает на X, который является const»: объект X нельзя изменить с помощью p.
  • X* const p означает «p – это константный указатель на X, который не является константой»: вы не можете изменить сам указатель p, но вы можете изменить объект Xс помощью p.
  • const X* const p означает, что «p – это константный указатель на X, который является константой»: вы не можете изменить сам указатель p, а также не можете изменить объект Xс помощью p.

И, о да, я уже упоминал, что нужно читать ваши объявления указателей справа налево?


Что означает "const X& x"?

Это означает, что x является ссылкой на объект X, но вы не можете изменить этот объект Xс помощью x.

Прочтите справа налево: «x – это ссылка на X, который является константой».

Например, если у класса X есть константная функция-член, такая как inspect() const, можно сказать x.inspect(). Но если класс X имеет неконстантную функцию-член с именем mutate(), то x.mutate() будет ошибкой.

Это полностью симметрично указателям на константу, включая тот факт, что компилятор выполняет всю проверку во время компиляции, что означает, что const не замедляет вашу программу и не требует от вас написания дополнительных тестовых случаев для проверки во время выполнения.


Что означают "X const& x" и "X const* p"?

X const& x эквивалентно const X& x, а X const* x эквивалентно const X* x.

Некоторые люди предпочитают стиль «const справа», называя его «последовательным const» (consistent const) или, используя термин, придуманный Саймоном Брэндом, «East const» (восточный const). Действительно, стиль «East const» может быть более последовательным, чем его альтернатива: стиль «East const» всегда помещает const справа от того, к чему он относится, тогда как другой стиль иногда помещает const слева, а иногда и справа (для объявлений константных указателей и константных функций-членов).

В стиле «East const» локальная переменная, являющаяся константой, определяется с помощью const справа: int const a = 42;. Точно так же статическая константная переменная определяется как static double const x = 3.14;. По сути, каждый const оказывается справа от того, к чему он относится, включая const, который должен быть справа: объявления константного указателя и константной функции-члена.

Стиль «East const» также менее запутан при использовании с псевдонимами типов: почему у foo и bar здесь разные типы?

using X_ptr = X*;

const X_ptr foo;
const X* bar;

Использование стиля «East const» делает это понятнее:

using X_ptr = X*;

X_ptr const foo;
X* const foobar;
X const* bar;

Здесь более ясно, что foo и foobar относятся к одному типу, а bar – к другому типу.

Стиль «East const» также более последователен с объявлениями указателей, в противоположность традиционному стилю:

const X** foo;
const X* const* bar;
const X* const* const baz;

И стиле «East const»

X const** foo;
X const* const* bar;
X const* const* const baz;

Несмотря на эти преимущества, стиль «const справа» еще не популярен, поэтому унаследованный код, как правило, использует традиционный стиль.


Имеет ли смысл "X& const x"?

Нет, это ерунда.

Чтобы узнать, что означает указанное выше объявление, прочтите его справа налево: «x – это константная ссылка на X». Но это излишне – ссылки всегда являются константными в том смысле, что вы никогда не можете переустановить ссылку, чтобы она ссылалась на другой объект. Никогда. С const или без него.

Другими словами, "X& const x" функционально эквивалентно "X& x". Поскольку вы ничего не получаете, добавляя const после &, вам не следует ее добавлять: это запутает людей – const заставит некоторых людей подумать, что X является константой, как если бы вы сказали "const X& x".


Что такое «константная функция-член»?

Функция-член, которая проверяет (а не изменяет) свой объект.

Константная функция-член обозначается суффиксом const сразу после списка параметров. Функции-члены с суффиксом const называются «константными функциями-членами» или «инспекторами» (inspector). Функции-члены без суффикса const называются «неконстантными функциями-членами» или «мутаторами» (mutator).

class Fred {
public:
  void inspect() const;   // эта функция-член обещает НЕ изменять *this
  void mutate();          // эта функция-член может изменять *this
};

void userCode(Fred& changeable, const Fred& unchangeable)
{
  changeable.inspect();   // ХОРОШО: не изменяет изменяемый объект
  changeable.mutate();    // ХОРОШО: изменяет изменяемый объект

  unchangeable.inspect(); // ХОРОШО: не изменяет неизменяемый объект
  unchangeable.mutate();  // ОШИБКА: попытка изменить неизменяемый объект
}

Попытка вызвать unchangeable.mutate() – это ошибка, обнаруживаемая во время компиляции. Для const не требуется места или скорости во время выполнения, и вам не нужно писать тестовые случаи для проверки во время выполнения.

Завершающий const в функции-члене inspect() должен использоваться для обозначения того, что метод не изменяет абстрактное (видимое клиенту) состояние объекта. Это немного отличается от того, чтобы сказать, что метод не изменяет «необработанные биты» структуры объекта. Компиляторам C++ не разрешается использовать «побитовую» интерпретацию, если они не могут решить проблему псевдонимов, которая обычно не может быть решена (т.е. может существовать неконстантный псевдоним, который может изменять состояние объекта). Еще один (важный) вывод из этой проблемы псевдонимов: указание на объект с помощью указателя на константу не гарантирует, что объект не изменится; он просто обещает, что объект не изменится с помощью этого указателя.


Какая связь между возвратом по ссылке и константной функцией-членом?

Если вы хотите вернуть член вашего объекта this по ссылке из метода-инспектора, вы должны вернуть его, используя ссылку на константу (const X& inspect() const) или по значению (X inspect() const).

class Person {
public:
  const std::string& name_good() const;  // Правильно: вызывающий не может изменить name в Person
  std::string& name_evil() const;        // Неправильно: вызывающий может изменить name в Person
  int age() const;                       // Тоже правильно: вызывающий не может изменить age в Person
  // ...
};

void myCode(const Person& p)  // myCode() обещает не изменять объект Person...
{
  p.name_evil() = "Igor";     // Но myCode() всё равно изменил его!!
}

Хорошая новость в том, что компилятор часто ловит вас, если вы ошиблись. В частности, если вы случайно вернете член объекта this по ссылке на неконстанту, например, в Person::name_evil() в коде выше, компилятор часто обнаруживает такое и выдает ошибку во время компиляции при компиляции внутренностей, в данном случае Person::name_evil().

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

Помните, что этот раздел пронизан «философией констант»: константная функция-член не должна изменять (или позволять вызывающей стороне изменять) логическое состояние (также известное как абстрактное состояние, или осмысленное состояние) объекта this. Подумайте о том, что означает объект, а не о том, как он реализован внутри. Возраст и имя человека логически являются частью человека (или почти дословно: «age и name объекта Person логически являются частью Person», прим. перевод.), но сосед и работодатель человека – нет. Метод-инспектор, который возвращает часть логического / абстрактного / осмысленного состояния объекта this, не должен возвращать неконстантный указатель (или ссылку) на эту часть, независимо от того, реализована ли эта часть внутри как прямой член данных, физически встроенный в объект this, или каким-то другим способом.


А в чем дело с «перегрузкой const»?

Перегрузка const помогает добиться корректности const.

Перегрузка const – это когда у вас есть метод-инспектор и метод-мутатор с одинаковыми именами и одинаковым количеством и типами параметров. Эти два различных метода отличаются только тем, что инспектор является константным, а мутатор – неконстантным.

Чаще всего перегрузка const используется с оператором индекса. Обычно вам следует попытаться использовать один из стандартных шаблонов контейнеров, например, std::vector, но если вам нужно создать свой собственный класс с оператором индекса, воспользуйтесь практическим правилом: операторы индекса часто идут парами.

class Fred { /*...*/ };

class MyFredList {
public:
  const Fred& operator[] (unsigned index) const;  // Операторы индекса часто идут в паре
  Fred&       operator[] (unsigned index);        // Операторы индекса часто идут в паре
  // ...
};

Константный оператор индекса возвращает константную ссылку, поэтому компилятор предотвратит непреднамеренное изменение Fred вызывающим объектом. Неконстантный оператор индекса возвращает неконстантную ссылку, которая является вашим способом сообщить вызывающим (и компилятору), что им разрешено изменять объект Fred.

Когда пользователь вашего класса MyFredList вызывает оператор индекса, компилятор выбирает, какую перегрузку вызвать, на основе константности MyFredList, имеющегося у вызывающего. Если у вызывающего есть MyFredList a или MyFredList& a, тогда a[3] вызовет неконстантный оператор индекса, и вызывающий получит неконстантную ссылку на Fred:

Например, предположим, что у класса Fred есть метод-инспектор inspect() const и метод-мутатор mutate():

void f(MyFredList& a)  // MyFredList - не константа
{
  // Можно вызвать методы, которые проверяют (просматривают, но не изменяют) Fred в a[3]:
  Fred x = a[3];       // Не меняется на Fred в a[3]: просто делает копию этого объекта Fred
  a[3].inspect();      // Не меняется на Fred в a[3]: inspect() const - это метод-инспектор
  
  // Можно вызывать методы, которые ДЕЙСТВИТЕЛЬНО изменяют Fred в a[3]:
  Fred y;
  a[3] = y;            // Изменяет Fred в a[3]
  a[3].mutate();       // Изменяет Fred в a[3]: mutate() - это метод-мутатор
}

Однако если у вызывающего есть const MyFredList a или const MyFredList& a, то a[3] вызовет константный оператор индекса, и вызывающий получит константную ссылку на Fred. Это позволяет вызывающей стороне проверять Fred в a[3], но предотвращает непреднамеренное изменение Fred в a[3].

void f(const MyFredList& a)  // MyFredList - константа
{
  // Можно вызвать методы, которые НЕ меняют Fred в a[3]:
  Fred x = a[3];
  a[3].inspect();

  // Ошибка времени компиляции (к счастью!), если вы попытаетесь изменить Fred в a[3]:
  Fred y;
  a[3] = y;       // К счастью(!) компилятор отловил эту ошибку во время компиляции
  a[3].mutate();  // К счастью(!) компилятор отловил эту ошибку во время компиляции
}

Перегрузка констант для оператор индекса и оператора funcall проиллюстрирована здесь, здесь, здесь, здесь и здесь. (ссылки скоро появятся)

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


Как это может помочь мне в лучшей разработке классов, если я отличаю логическое состояние от физического?

Потому что это побуждает вас разрабатывать свои классы «снаружи вовнутрь», а не «изнутри наружу», что, в свою очередь, делает ваши классы и объекты более простыми для понимания и использования, более интуитивными, менее подверженными ошибкам и более быстрыми. (Да, это немного упрощенно. Чтобы понять все «если», «и» и «но», вам просто нужно прочитать оставшуюся часть этого ответа!)

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

Внутри ваши объекты имеют физическое (или конкретное, или побитовое) состояние. Это состояние, которое программистам легко увидеть и понять; это состояние, которое было бы, если бы класс был просто структурой в стиле C.

Снаружи у ваших объектов есть пользователи вашего класса, и эти пользователи могут использовать только общедоступные функции-члены и друзей. Эти внешние пользователи также воспринимают объект как имеющий состояние, например, если объект относится к классу Rectangle с методами width(), height() и area(), ваши пользователи скажут, что все они являются частью логического (или абстрактного, или осмысленного) состояния. Для внешнего пользователя объект Rectangle на самом деле имеет площадь, даже если эта площадь вычисляется на лету (например, если метод area() возвращает произведение ширины и высоты объекта). Фактически, и это важный момент, ваши пользователи не знают и не заботятся о том, как вы реализуете какой-либо из этих методов; ваши пользователи по-прежнему воспринимают, со своей точки зрения, что ваш объект логически имеет осмысленное состояние ширины, высоты и площади.

Пример area() показывает случай, когда логическое состояние может содержать элементы, которые не реализуются напрямую в физическом состоянии. Верно и обратное: классы иногда намеренно скрывают часть физического (конкретного, побитового) состояния своих объектов от пользователей – они намеренно не предоставляют никаких общедоступных функций-членов или друзей, которые позволили бы пользователям читать или писать или даже знать об этом скрытом состоянии. Это означает, что в физическом состоянии объекта есть биты, которым нет соответствующих элементов в логическом состоянии объекта.

В качестве примера последнего случая, объект-коллекция может кешировать свой последний поиск в надежде улучшить производительность своего следующего поиска. Этот кеш, безусловно, является частью физического состояния объекта, но это внутренняя деталь реализации, которая, вероятно, не будет открыта пользователям, – она, вероятно, не будет частью логического состояния объекта. И этот же случай, если подумать «снаружи вовнутрь»: если пользователи объекта-коллекции не имеют возможности проверить состояние самого кеша, то кеш прозрачен и не является частью логического состояния объекта.


«Постоянство» моих общедоступных функций-членов должно основываться на том, что метод делает с логическим или физическим состоянием объекта?

С логическим.

Следующую часть невозможно упростить. Будет больно. Вам лучше сесть. И, пожалуйста, для вашей безопасности убедитесь, что поблизости нет острых предметов.

Вернемся к примеру объекта-коллекции. Помните: есть метод поиска, который кеширует последний поиск в надежде ускорить поиск в будущем.

Давайте сформулируем то, что, вероятно, очевидно: предположим, что метод поиска не изменяет ни одно из логических состояний объекта-коллекции.

Итак... пришло время причинить вам боль. Вы готовы?

А вот и: если метод поиска не вносит никаких изменений в какое-либо логическое состояние объекта-коллекции, но он меняет физическое состояние объекта-коллекции (он вносит очень реальное изменение в очень реальный кеш), должен ли метод поиска быть константным?

Ответ однозначный: да. (Из каждого правила есть исключения, поэтому у этого «да» на самом деле должна быть рядом звездочка, но в подавляющем большинстве случаев ответ – да.)

Все дело в преимуществе «логического постоянства» над «физическим постоянством». Это означает, что решение о том, украшать ли метод с помощью const, должно зависеть в первую очередь от того, оставляет ли этот метод логическое состояние неизменным, независимо от того (вы сидите?), независимо от того, делает ли метод очень реальные изменения очень реального физического состояния объекта.

Если это не доходит до вас, или если вы еще не испытываете боли, давайте разделим это на два случая:

  • если метод изменяет какую-либо часть логического состояния объекта, он логически является мутатором; он не должен быть константным, даже если (на самом деле!) этот метод не изменяет никаких физических битов конкретного состояния объекта;
  • и наоборот, метод логически является инспектором и должен быть константным, если он никогда не изменяет какую-либо часть логического состояния объекта, даже если (на самом деле!) этот метод изменяет физические биты конкретного состояния объекта.

Если вы запутались, прочтите еще раз.

Если вы не в замешательстве, но злитесь – хорошо: может вам это еще не нравится, но, по крайней мере, вы это понимаете. Сделайте глубокий вдох и повторяйте за мной: «Постоянство метода должно иметь смысл извне объекта».

Если вы всё еще злитесь, повторите это трижды: «Постоянство метода должно иметь смысл для пользователей объекта, а эти пользователи могут видеть только логическое состояние объекта».

Если вы всё еще злитесь, извините, что есть, то есть. Смиритесь и живите с этим. Да, будут исключения; они есть в каждом правиле. Но, как правило, в основном это понятие логической константы полезно для вас и для вашего программного обеспечения.

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


Что мне делать, если я хочу, чтобы константная функция-член вносила «невидимые» изменения в член данных?

Используйте mutable (или, в крайнем случае, используйте const_cast).

Небольшому проценту инспекторов необходимо вносить изменения в физическое состояние объекта, которое не может быть замечено внешними пользователями, – изменение физического, но не логического состояния.

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

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

Чтобы помочь вам принять это понятие логического постоянства, язык C++ использует ключевое слово mutable. В этом случае вы должны пометить кеш ключевым словом mutable, чтобы компилятор знал, что его разрешено изменять внутри константного метода или через любой другой константный указатель или ссылку. То есть ключевое слово mutable отмечает те части физического состояния объекта, которые не являются частью логического состояния.

Ключевое слово mutable идет непосредственно перед объявлением элемента данных, то есть там же, где вы могли бы поместить const. Другой подход, который не является предпочтительным, состоит в том, чтобы отбросить постоянство указателя на this, возможно, с помощью ключевого слова const_cast:

Set* self = const_cast<Set*>(this);
// Прежде чем так поступать, прочитайте ПРИМЕЧАНИЕ!

После этой строки self будет иметь те же биты, что и this, то есть self == this, но self – это Set*, а не const Set* (технически это const Set* const, но крайний правый const не имеет отношения к этому обсуждению). Это означает, что вы можете использовать self для изменения объекта, на который указывает this.

ПРИМЕЧАНИЕ: крайне маловероятная ошибка, которая может возникнуть с const_cast. Это происходит только тогда, когда одновременно объединяются три очень редких вещи: член данных, который должен быть изменяемым (например, как обсуждалось выше), компилятор, который не поддерживает ключевое слово mutable, и/или программист, который не использует его, и объект, который изначально был определен как константа (в отличие от обычного неконстантного объекта, на который указывает указатель на константу). Хотя эта комбинация настолько редка, что она может никогда не случиться с вами, но если она когда-либо случится, код может не работать (стандарт говорит, что поведение в этом случае не определено).

Если вы когда-нибудь захотите использовать const_cast, используйте вместо него mutable. Другими словами, если вам когда-либо понадобится изменить член объекта, и на этот объект указывает указатель на константу, самое безопасное и простое, что можно сделать, – это добавить mutable в объявление члена. Вы можете использовать const_cast, если уверены, что реальный объект не является константным (например, если вы уверены, что объект объявлен примерно так: Set s;), но если сам объект может быть константным (например, если он может быть объявленным как: const Set s;), используйте mutable вместо const_cast.

Пожалуйста, не пишите, что версия X компилятора Y на машине Z позволяет вам изменять не mutable член константного объекта. Мне это неинтересно – в соответствии с языком это нелегально, и ваш код, вероятно, перестанет работать с другим компилятором или даже в другой версии (обновлении) этого же компилятора. Просто нет. Используйте лучше mutable. Пишите код, который гарантированно будет работать, а не код, который, кажется, не ломается.


Означает ли const_cast потерю возможностей оптимизации?

Теоретически да; на практике нет.

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


Почему компилятор позволяет мне изменить int после того, как я указал на него с помощью const int*?

Потому что "const int* p" означает «p обещает не изменять *p», а не «*p обещает не изменяться».

Использование const int* для указания на int не делает int константным. int нельзя изменить с помощью const int*, но если у кого-то есть int* (примечание: без const), который указывает на («псевдоним») тот же int, то этот int* можно использовать для изменения int. Например:

void f(const int* p1, int* p2)
{
  int i = *p1;         // Получить (исходное) значение of *p1
  *p2 = 7;             // Если p1 == p2, это также изменить и *p1
  int j = *p1;         // Получить (возможно, новое) значение *p1
  if (i != j) {
    std::cout << "*p1 changed, but it didn't change via pointer p1!\n";
    assert(p1 == p2);  // Это единственный способ, почему *p1 может измениться
  }
}

int main()
{
  int x = 5;
  f(&x, &x);           //  Это совершенно законно (и даже морально!)
  // ...
}

Обратите внимание, что main() и f(const int*, int*) могут находиться в разных блоках компиляции, которые компилируются в разные дни недели. В этом случае компилятор не сможет обнаружить псевдонимы во время компиляции. Следовательно, мы не можем создать языковое правило, запрещающее подобные вещи. Фактически, мы даже не хотели бы устанавливать такое правило, поскольку в целом считается, что у вас может быть много указателей, указывающих на одно и то же. Тот факт, что один из этих указателей обещает не изменять лежащую в его основе «штуковину», – это просто обещание, данное указателем; это не обещание, данное «штуковиной».


Означает ли "const Fred* p", что *p не может измениться?

Нет! (Это связано с ответом на вопрос о псевднимах указателей на int.)

"const Fred* p" означает, что Fred не может быть изменен с помощью указателя p, но могут быть другие способы добраться до объекта без использования const (например, неконстантный указатель с псевдонимом, такой как Fred*) . Например, если у вас есть два указателя "const Fred* p" и "Fred* q", которые указывают на один и тот же объект Fred (псевдонимы), указатель q может использоваться для изменения объекта Fred, но указатель p – нет.

class Fred {
public:
  void inspect() const;   // Константная функция-член
  void mutate();          // Неконстантная функция-член
};

int main()
{
  Fred f;
  const Fred* p = &f;
  Fred*       q = &f;

  p->inspect();    // Хорошо: нет изменений в *p
  p->mutate();     // Ошибка: не могу изменить *p через p

  q->inspect();    // Хорошо: указателю q позволено проверять объект
  q->mutate();     // Хорошо: указателю q позволено изменять объект

  f.inspect();     // Хорошо: указателю f позволено проверять объект
  f.mutate();      // Хорошо: указателю f позволено изменять объект
  // ...
}

Почему я получаю сообщение об ошибке при преобразовании Foo**const Foo**?

Потому что преобразование Foo**const Foo** было бы недопустимым и опасным.

C++ позволяет (безопасное) преобразование Foo*Foo const*, но выдает ошибку, если вы пытаетесь неявно преобразовать Foo**const Foo**.

Обоснование того, почему эта ошибка оправдана, приводится ниже. Но сначала вот наиболее распространенное решение: просто замените const Foo** на const Foo* const*:

class Foo { /* ... */ };

void f(const Foo** p);
void g(const Foo* const* p);

int main()
{
  Foo** p = /*...*/;
  // ...
  f(p);  // ОШИБКА: незаконно и аморально преобразовывать Foo** в const Foo**
  g(p);  // ХОРОШО: законно и морально преобразовывать Foo** в const Foo* const*
  // ...
}

Причина, по которой преобразование Foo**const Foo** опасно, состоит в том, что оно позволит вам незаметно и случайно изменить объект const Foo без приведения:

class Foo {
public:
  void modify();  // выполняет какие-то изменения в данном объекте
};

int main()
{
  const Foo x;
  Foo* p;
  const Foo** q = &p;  // q указывает на p; это (к счастью!) ошибка
  *q = &x;             // p теперь указывает на x
  p->modify();         // Упс: изменяет const Foo!!
  // ...
}

Если бы строка q = &p была допустимой, q указывал бы на p. Следующая строка, *q = &x, изменяет сам p (поскольку *q - это p), чтобы он указывал на x. Это было бы плохо, поскольку мы потеряли бы квалификатор const: p – это Foo*, а x – это const Foo. Строка p->modify() использует способность p изменять свой объект ссылки, что является настоящей проблемой, поскольку мы закончили тем, что изменили const Foo.

По аналогии, если вы скрываете преступника под законной маскировкой, он может затем использовать доверие, оказанное этой маскировкой. Это плохо.

К счастью, C++ не позволяет так делать: строка q = &p помечается компилятором C++ как ошибка времени компиляции. Напоминание: пожалуйста, не используйте приведение указателя, чтобы обойти это сообщение об ошибке времени компиляции. Просто нет!

(Примечание: есть концептуальное сходство между этим и запретом на преобразование Derived** в Base**.)

Теги

C++ / CPPconstFAQВысокоуровневые языки программированияСсылка / Reference (программирование)Указатель / Pointer (программирование)Языки программирования

На сайте работает сервис комментирования DISQUS, который позволяет вам оставлять комментарии на множестве сайтов, имея лишь один аккаунт на Disqus.com.

В случае комментирования в качестве гостя (без регистрации на disqus.com) для публикации комментария требуется время на премодерацию.