24

Хотелось бы понять, как С++ обрабатывает константы базовых типов.

Что будет, если при помощи плясок с бубном и указателями изменить значение, находящееся в ячейке памяти, где, по идее, и должна содержаться константа?

Есть следующий код:

#include <iostream>

using namespace std;

int main(int argc, char** argv) {
    const int number = 45;
    const int * constPoint = &number;
    cout << "constPoint " << constPoint << endl;
    int * point = (int *) constPoint;
    cout << "     point " << point << endl;
    * point = 54;
    cout << "&number " << &number << "; number " << number << endl;
    cout << "  point " << point   << "; *point " << *point << endl;
}

Вывод у меня получается следующий:

constPoint 0x7fff1b73a6ec
     point 0x7fff1b73a6ec
&number 0x7fff1b73a6ec; number 45
  point 0x7fff1b73a6ec; *point 54

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

Как объяснить подобное поведение?

Я не собираюсь такой код никогда использовать на практике, просто хочу понять, где хранятся константы.

ixSci
  • 23,825

3 Answers3

19

Для начала: по стандарту, изменение при помощи трюков с указателями констант есть undefined behaviour. Может случится всё, что угодно в любой точке программы.

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

Например, если компилятор видит обращение к константе, он может встроить её в точку вызова (он ведь знает значение на этапе компиляции), может обратиться к этой константе по адресу в любой момент, может даже выразить другие константы через эту (например, если у вас есть константа 1024, то обращение к 2048 он имеет право представить как 1024 << 1 из соображений эффективности).

Хуже того, если где-то компилятор выяснил для себя, что константа чётная, и её младший бит равен 0, и на основании этой информации смог исключить какие-нибудь проверки из кода, то теперь проверки не пройдут, и код может крешнуться в любой момент.

На UB обычно основаны дыры в безопасности программ. Например, если компилятор обоснованно считает, что длина строки 45, и под неё достаточно выделить буфер такой длины, в то время как вы, обманув компилятор, подсунете ему строку длиной 54, получится классический срыв стека.


Резюме: У компилятора нет никаких «обязательств» или «принципов» по работе с константами. Он имеет право делать что угодно. Наоборот, это программист, если уж обещал, что какое-то значение есть константа, не имеет права его менять.

VladD
  • 206,799
  • А если сделать volatile const? Вроде где-то писалось, что это нормально. – Qwertiy Jul 18 '15 at 21:59
  • 2
    @Qwertiy: Эээ, а какой тогда смысл const, если его можно менять? – VladD Jul 18 '15 at 22:44
  • 3
    Это как бы утверждение, что мы это не меняем и при явной попытке присваивания должна быть ошибка компиляции. Но при этом мы говорим, не смей оптимизировать доступ, внешний код может в любой момент поменять значение. – Qwertiy Jul 18 '15 at 22:46
9

Согласно стандарту C++ (7.1.6.1 The cv-qualifiers)

4 Except that any class member declared mutable (7.1.1) can be modified, any attempt to modify a const object during its lifetime (3.8) results in undefined behavior

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

В C++ строковые литералы имеют типы константных символьных массивов. Например, строковый литерал "Hello" имеет тип const char[6]. Однако в C строковые литералы имеют типы неконстантных символьных массивов. Поэтому этот же самый строковый литерал "Hello" в C имеет тип char[6]. Тем не менее вы не можете изменить строковый литерал в C, также как вы не можете изменить строковый литерал в C++.

Из стандарта C (6.4.5 String literals)

7 It is unspecified whether these arrays are distinct provided their elements have the appropriate values. If the program attempts to modify such an array, the behavior is undefined.

То, что в C строковые литералы имеют типы неконстантных символьных массивов, очевидно связано с поддержкой уже существующей кодовой базы на момент введения квалификатора const в стандарт языка C.

5

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

volatile const int number = 45;

Полный код: http://codepad.org/tNlZoNkE
Вариант с кастом в void* для вывода: http://codepad.org/EoqOgd6t

PS: Насколько я помню, volatile const это корректная ситуация, которая не должна приводить к undefined behavior.

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

Для volatile это неважно. Представим, что есть нечто внешнее, что "совершенно случайно" меняет ту же константу на те же значения, что мы написали. А в самой программе вместо изменяющего кода стоят nop'ы. Такой вариант абсолютно точно корректен и не вызывает UB.

Значит, если UB есть, то его вызывает не изменившееся значение, а сама операция записи. Есть ли какой-то случай, в котором это возможно? Приходит в голову только read-only страница в памяти, но может ли компилятор так поступить? Локальную переменную он поместит в стек, который однозначно не readonly. Остаются глобальные. (Поля объектов попадают в одну из этих категорий.) Или есть ещё ситуации, когда операция записи может что-то испортить?

Qwertiy
  • 123,725
  • 1
    Сможете привести ссылку на стандарт? В ответе Vlad from Moscow утверждается со ссылкой, что менять const нельзя. – VladD Jul 18 '15 at 22:49
  • 1
    Я просмотрел секцию 7.1.6.1 стандарта и не нашёл там подтверждения, что const volatile-объект можно менять. Там лишь есть указание на то, что он имеет право поменяться внешним для программы образом, что не одно и то же. – VladD Jul 18 '15 at 22:57
  • @VladD, нашёл только такую информацию: http://www.phy.duke.edu/~rgb/General/c_book/c_book/chapter8/const_and_volatile.html#section-2.1 http://en.cppreference.com/w/cpp/language/cv http://embeddedgurus.com/barr-code/2012/01/combining-cs-volatile-and-const-keywords/ В третьей ссылке один из примеров - общение двух обработчиков через разделяемую память, когда мы только читаем. – Qwertiy Jul 18 '15 at 23:07
  • @VladD, вообще, моя мысль такая: изменение значения нанести вред не может, поскольку компилятор не имеет права предсказывать значение volatile - именно для этого он и предназначен. Что остаётся? Запись в память. Может ли она что-то сломать? Вроде тоже не должна. И ещё один момент. Как можно различить свой и внешний код? А если мы получили тот же указатель откуда-то ещё? – Qwertiy Jul 18 '15 at 23:09
  • 1
    Ну. различить свой и внешний код просто: компилятор имеет доступ ко всему своему коду, и может провести whole program optimization. В процессе этой оптимизации он имеет полное право предполагать, что свой код точно не меняет const-объекты. Если это не так, это может привести к проблемам. – VladD Jul 18 '15 at 23:56
  • Просмотрел ваши ссылки. По первой память меняет interrupt handler, или память мапится на машинный регистр. Пример в третьей более интересный, да. Можно ли считать различный части ОС одним компилятом? (Однако, в embedded-среде, в контексте которой пишет третий автор, традиционно семантика volatile довольно сильная, а оптимизирующие компиляторы довольно слабые, так что...) – VladD Jul 19 '15 at 00:13
  • @VladD, дополнил ответ. – Qwertiy Jul 19 '15 at 00:30
  • 2
    Хм. Мне кажется, ваше рассуждение об UB неверно, компилятор работает не так. Он имеет право предполагать, что UB никогда не случается. Поэтому если он сможет доказать, что код модифицирует const-переменную, он имеет полное право считать, что этот код никогда не будет выполнен, и выбросить его. – VladD Jul 19 '15 at 00:39
  • Вот немного про UB: http://blog.regehr.org/archives/213 – VladD Jul 19 '15 at 00:50
  • @VladD, но ведь изменяемый указатель будет указывать тоже на volatile. Может ли компилятор посчитать volatile-запись за UB, или же он обязан записать? – Qwertiy Jul 20 '15 at 22:18
  • 1
    Запись в const есть UB. Цитата из статьи: Undefined behavior trumps all other behaviors of the C abstract machine. То есть в присутствии UB по идее компилятор не обязан записывать. То есть фактически ничего не обязан. – VladD Jul 20 '15 at 22:22
  • @VladD, кто тогда может менять такие константы? Только меппинг на hardware? – Qwertiy Jul 20 '15 at 22:24
  • Ну, по идее их может менять другой процесс. Ну и hardware mapping, да. Возникает интересный вопрос, как рассматривать динамическую библиотеку, у которой есть ссылка на тот же адрес как не-const. Хотя фактически в рантайме никто не будет париться, и динамическая библиотека сможет поменять константу без проблем — но как на это смотрит стандарт? – VladD Jul 20 '15 at 22:25
  • Потому что когда компилируется динамическая библиотека, компилятор не имеет права знать, по идее, что модуль, который её загрузит, имеет view на эту область памяти как const. Но здесь я не спец. – VladD Jul 20 '15 at 22:27
  • Другое дело, что обычно такие штуки нужны в embedded-системах, а там компиляторы обычно доопределяют UB. Ну и там обычно все плюют на const и тому подобное. – VladD Jul 20 '15 at 22:30
  • 1
    @VladD, задал вопрос на английском SO: http://stackoverflow.com/q/31527499/4928642 – Qwertiy Jul 20 '15 at 22:38
  • Угу. там есть интересная часть насчёт «initially defined». Я понял это так: если объект по сути есть const, то у нас UB. А если объект — не const, на нам на него достался const-указатель, то убрать const нормально. – VladD Jul 20 '15 at 23:04
  • Вопрос, опять-таки, в том, что есть для компилятора «по сути». – VladD Jul 20 '15 at 23:04
  • То есть начальное определение volatile const int x = 42;, судя по всему, означает UB, т. к. объект x, на который потом будет указатель, с точки зрения компилятора реально является константой. А вот что означает начальное определение const volatile int *p = (const volatile int*)0x12340000;? Указатель есть , а объекта как бы и нету. – VladD Jul 20 '15 at 23:06
  • @VladD, начальное определение не может быть UB - это же инициализация. А вот неинициализированная константа - это что-то странное. Вторая запись - это приведение числового адреса hardware-регистра к указателю на volatile const int. – Qwertiy Jul 20 '15 at 23:09
  • Я имел в виду, если начальное определение объекта с const (volatile const int x = 42;), то изменение его есть UB. (Мозги уже отправились спать, пишу на автомате.) Вне зависимости от того, через какие указатели передавался адрес. – VladD Jul 20 '15 at 23:10
  • А вот если начального определения объекта нету, а есть только указатель (const volatile int *p = (const volatile int*)0x12340000;), то тут становится интересно. – VladD Jul 20 '15 at 23:13