Семантика указателей/ссылок на 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 шт):

Автор решения: Arenoros

Вообще 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 и почему нас должно это волновать?

→ Ссылка
Автор решения: Fat-Zer

В стандарте (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++ всё примерно также, но перерывать более толстый стандарт в поисках этого мне не сильно хочется...

→ Ссылка