Семантика указателей/ссылок на const в языках C и C++
Подскажите, пожалуйста, что говорят стандарты C и C++ о семантике указателя/ссылки на константу?
(Да, я знаю, что в C нет ссылок)
Меня смущают следующие неясности:
Кто-то говорит, что указатель/ссылка на константное значение - это лишь путь доступа к объекту. То есть, указатель/ссылка на константу - это фильтр, который запрещает менять целевой объект (на этапе компиляции). Но, в то же время, через указатель/ссылку на константу можно увидеть факт изменения состояния целевого объекта.
Кто-то говорит, что компилятор может решить, что указатель/ссылка на константное значение обозначает, что целевое значение не может быть изменено.
Например:
// Somewhere far. int source_value = 0; some_function(&source_value, &source_value); // Somewhere farther. void some_function(const int* a, int* b) { cout << *a << endl: *b = 1; cout << *a << endl;// Do we have guarantee that *a has 1? }С другой стороны, существуют Правила Строгих Псевдонимов (
Strict Aliasing Rules), которые явно разрешают делать псевдоним для типа через указатель/ссылку наcv-квалифицированныйвариант целевого типа. На мой взгляд, это правило подтверждает тот факт, что указатель/ссылка на константное значение может использоваться для обнаружения факта изменения целевого объекта, даже если код, в котором происходит наблюдение, понятия не имеет о том, что исходный объект является изменяемым и в принципе способен изменить свое состояние.
Я пытался читать черновики стандартов C11 и C++17, но мне не удалось найти информацию, которая развеяла бы все мои сомнения.
Ответы (2 шт):
Вообще const говорит лишь о том что мы не планируем в этой части кода менять переменную. И в реальности компилятор не делает ни каких оптимизаций основываясь лишь на этом.
Strict Aliasing Rules это про другое, оно говорит о том что ссылки на "совершенно разные" типы не должны пересекаться.
void foo(const float* f , int* i){
*i = 10;
printf("%f\n", *f);
}
int main(){
float f = 100;
int *i = (int*)&f; // нарушение Strict Aliasing Rules
foo(&f, i); // UB
return 0;
}
Про strict aliasing есть более подробная статья на habr: Что такое Strict Aliasing и почему нас должно это волновать?
В стандарте (C11) о семантике объектов const-qualified типов нам по сути даны только два ограничения*:
- Из 6.3.2.1/1: они не являются modifiable lvalue, и, как следствие, не могут использоваться в присваиваниях, инкремента и т.п.
- Из 6.7.3/6: попытка изменить из через не константный указатель — это UB.
В остальном они и указатели на них ведут себя точно также как и обычные объекты/указатели. Для них нет ни каких особых исключений.
*Есть ещё пара свобод компилятору по размещению const-объектов: в памяти данных, встраиванию или то что они могут накладываться друг на друга, но это к делу не относится.
Отсюда, по конкретным утверждениям:
Кто-то говорит, что указатель/ссылка на константное значение - это лишь путь доступа к объекту. То есть, указатель/ссылка на константу - это фильтр, который запрещает менять целевой объект (на этапе компиляции). Но, в то же время, через указатель/ссылку на константу можно увидеть факт изменения состояния целевого объекта.
Если не придираться к словам, всё правильно.
Кто-то говорит, что компилятор может решить, что указатель/ссылка на константное значение обозначает, что целевое значение не может быть изменено.
Вообще говоря, это не верно.
С другой стороны, существуют Правила Строгих Псевдонимов…
Не то что подтверждает, но не противоречит и написано так, чтобы пример ниже не создавал проблем с SA...
По примеру:
void some_function(const int* a, int* b) {
*b=1;
printf("%d\n",*a); // если a==b, то 1
}
Теперь по порядку, что тут происходит с точки зрения Си:
*b=1иprintf ("%d\n",*a)находятся в разных операторах (statement).- Согласно annex С, это значит, что между ними есть точка следования (sequence point).
- Согласно 5.1.2.3/2, модификация объекта
*b=1— это побочный эффект. - И теперь согласно 5.1.2.3/3, т.к. выражения
*b=1иprintf("%d\n",*a)разделены точкой следования, то все побочные эффекты вычисления*b=1должны предшествовать (sequenced before) выражениюprintf("%d\n",*a).
Т.е. стандарт гарантирует, что в этом примере при a==b будет выведено 1. Это не требует от компилятора выдавать машинный код, который в точности выполняет все действия, в описанной последовательности, однако основное правило оптимизации в том, что она не изменяет видимое поведение корректной программы. Так что в данном случае компилятор не вправе сначала получить значение *a и только потом изменить *b, не зависимо от того, есть ли const или нет.
Но такие строгости не всегда нужны и они не позволяют делать некоторые оптимизации. И дабы преодолеть это введены две вещи:
Пресловутые правила
Strict Aliasing. С ними компилятор вправе принимать на веру, что для разных типов указатели указывают на разные объекты т.к. в противном случае виноват программист, а он, компилятор, всё сделал «как надо»:void some_function(int* a, float* b) { *b=1; printf("%d\n",*a); // если a==b, то UB }В С99 появился спецификатор
restrict(в плюсах его нет, но большинство компиляторов поддерживают нестандартное ключевое слово__restrict). С ним, точно также, компилятор вправе считать, что указатели указывают на разные объекты:void some_function(int* restrict a, int* restrict b) { *b=1; printf("%d\n",*a); // если a==b, то UB }
В C++ всё примерно также, но перерывать более толстый стандарт в поисках этого мне не сильно хочется...