От переводчика:
Предлагаю вам перевод поста из блога Мэтта Стэнклиффа (Matt Stancliff), автора нашумевшей на хабре статьи Советы о том, как писать на С в 2016 году.
Здесь Мэтт делится знаниями о квалификаторе типа const. Несмотря на вызывающий заголовок, возможно, многое из того что здесь описывается будет вам известно, но, надеюсь, и что-нибудь новое тоже найдется.
Приятного чтения.


Думаете, что вы знаете все правила использования const для С? Подумайте еще раз.

Основы const


Скалярные переменные


Вы знакомы с простым правилом const в С.
const uint32_t hello = 3;

const перед hello означает, что во время компиляции происходит проверка того, что hello никогда не меняется.

Если вы попытаетесь изменить или переопределить hello, компилятор остановит вас:
clang-700.1.81:
error: read-only variable is not assignable
    hello++;
    ~~~~~^
error: read-only variable is not assignable
    hello = 92;
    ~~~~~ ^


gcc-5.3.0:
error: increment of read-only variable 'hello'
     hello++;
          ^
error: assignment of read-only variable 'hello'
     hello = 92;
           ^

Кроме того, C не сильно беспокоится о том, где расположен const до тех пор пока он находится перед идентификатором, так что объявления const uint32_t и uint32_t const идентичны:
const uint32_t hello = 3;
uint32_t const hello = 3;

Скалярные переменные в прототипах


Сравните прототип и реализацию следующей функции:
void printTwo(uint32_t a, uint64_t b);

void printTwo(const uint32_t a, const uint64_t b) {
    printf("%" PRIu32 " %" PRIu64 "\n", a, b);
}

Будет ли ругаться компилятор, если в реализации функции printTwo() указаны скалярные параметры с квалификатором const, а в прототипе без него?

Неа.

Для скалярных аргументов совершенно нормально, что квалификаторы const не совпадают в прототипе и реализации функции.
Почему это хорошо? Все очень просто: ваша функция никак не может изменить a и b вне своей области видимости, поэтому const не оказывает никакого влияния то что вы ей передаете. Ваш компилятор достаточно умен, чтобы понять, что это будут копии a и b, так что в данном случае наличие или отсутствие const не оказывает никакого влияния на физические или ментальные модели вашей программы.

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

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

Массивы


Вы можете указать const для всего массива.
const uint16_t things[] = {5, 6, 7, 8, 9};

const также может указываться после объявления типа:
uint16_t const things[] = {5, 6, 7, 8, 9};

Если вы попытаетесь изменить things[], компилятор остановит вас:
clang-700.1.81:
error: read-only variable is not assignable
    things[3] = 12;
    ~~~~~~~~~ ^

gcc-5.3.0:
error: assignment of read-only location 'things[3]'
     things[3] = 12;
               ^


Структуры


Обычные структуры


Вы можете указать const для всей структуры.
struct aStruct {
    int32_t a;
    uint64_t b;
};

const struct aStruct someStructA = {.a = 3, .b = 4};

Или:
struct const aStruct someStructA = {.a = 3, .b = 4};

Если мы попытаемся изменить какой-либо член someStructA:
someStructA.a = 9;

Мы получим ошибку, т.к. someStructA объявлена как const. Мы не можем изменять её члены после определения.
clang-700.1.81:
error: read-only variable is not assignable
    someStructA.a = 9;
    ~~~~~~~~~~~~~ ^

gcc-5.3.0:
error: assignment of member 'a' in read-only object
     someStructA.a = 9;
                   ^

const внутри структуры


Вы можете указать const для отдельных членов структуры:
struct anotherStruct {
    int32_t a;
    const uint64_t b;
}; 

struct anotherStruct someOtherStructB = {.a = 3, .b = 4}; 

Если мы попытаемся изменить какие-либо члены someOtherStructB:
someOtherStructB.a = 9;
someOtherStructB.b = 12;

Мы получим ошибку только при изменении b, т.к. b объявлена как const:
clang-700.1.81:
error: read-only variable is not assignable
    someOtherStructB.b = 12;
    ~~~~~~~~~~~~~~~~~~ ^

gcc-5.3.0:
error: assignment of read-only member 'b'
     someOtherStructB.b = 12;

Объявление всего экземпляра структуры с квалификатором const равносильно объявлению специальной копии структуры, в которой все члены определены как const. Если вам не нужна 100% const структура, вы можете указать const только для конкретных членов при объявлении структуры, только там где это необходимо.

Указатели


const для указателей — вот где начинается веселье.

Один const


Давайте использовать указатель на целое число в качестве примера.
uint64_t bob = 42;
uint64_t const *aFour = &bob;

Так как это указатель, то здесь присутствуют два хранилища:
  • Хранилище данных — bob
  • Хранилище указателя aFour, указывающего на bob

Итак, что мы можем сделать с aFour? Давайте попробуем несколько вещей.
Вы думаете, что значение на которое он указывает можно изменять?
*aFour = 44;

clang-700.1.81:
error: read-only variable is not assignable
    *aFour = 44;
    ~~~~~~ ^

gcc-5.3.0:
error: assignment of read-only location '*aFour'
     *aFour = 44;
            ^

Как насчет обновления const-указателя без изменения значения на которое он указывает?
aFour = NULL;

Это действительно работает и вполне допустимо. Мы объявили uint64_t const *, что означает «указатель на неизменяемые данные», но сам по себе указатель не является неизменяемым (заметьте также: const uint64_t * имеет тоже значение).

Как сделать неизменяемыми одновременно и данные и указатель? Знакомьтесь: двойной const.

Два const


Давайте добавим ещё один const и посмотрим как пойдут дела.
uint64_t bob = 42;
uint64_t const *const anotherFour = &bob;

*anotherFour = 45;
anotherFour = NULL;

Что в итоге?
clang-700.1.81:
error: read-only variable is not assignable
    *anotherFour = 45;
    ~~~~~~~~~~~~ ^
error: read-only variable is not assignable
    anotherFour = NULL;
    ~~~~~~~~~~~ ^

gcc-5.3.0:
error: assignment of read-only location '*anotherFour'
     *anotherFour = 45;
                  ^
error: assignment of read-only variable 'anotherFour'
     anotherFour = NULL;
                 ^

Ага, у нас получилось сделать и данные, и сам указатель неизменяемыми.

Что означает const *const?
Значение тут кажется менее очевидным.
Значение настолько шатко, потому что на самом деле рекомендуется читать объявления переменных справа налево (или ещё хуже, спиралью).
В данном случае, если читать справа налево2, это объявление означает:
uint64_t const *const anotherFour = &bob;

anotherFour это:
  • неизменяемый указатель (*const)
  • на неизменяемую переменную (uint64_t const)

Возьмем наш «обычный» синтаксис и прочитаем справа налево:
uint64_t const *aFour = &bob;

aFour это:
  • обычный, изменяемый указатель (* означает, что сам указатель может изменяться)
  • на неизменяемую переменную (uint64_t const означает, что данные не могут изменяться)

Что мы только что видели?
Здесь есть важное различие: люди обычно называют const uint64_t *bob как «неизменяемый указатель», но это не то что здесь происходит. На самом деле это «изменяемый указатель на неизменяемые данные».

Интерлюдия — объясняем объявления const


Но подождите, дальше — больше!

Мы только что видели как представление указателя дало нам четыре различных варианта для объявления квалификатора const. Мы можем:
  • Не объявлять ни одного const и позволить изменять и сам указатель и данные на которые он указывает
    uint64_t *bob;
    

  • Объявить неизменяемыми только данные, но позволить изменять указатель
    uint64_t const *bob;
    

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

  • Объявить неизменяемым только указатель, но позволить изменять данные
    uint64_t *const bob;
    

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

  • Объявить неизменяемыми указатель и данные, запретив изменять их после инициализирующего объявления
    uint64_t const *const bob;
    


Это то, что касается одного указателя и двух const, но что если мы добавим ещё один указатель?

Три const


Один


Сколько способов мы можем использовать, чтобы добавить const к двойному указателю?

Давайте быстро это проверим.
uint64_t const **moreFour = &aFour;

Какие из этих операций допускаются, исходя из объявления выше?
**moreFour = 46;
*moreFour = NULL;
moreFour = NULL;

clang-700.1.81:
error: read-only variable is not assignable
    **moreFour = 46;
    ~~~~~~~~~~ ^

gcc-5.3.0:
error: assignment of read-only location '**moreFour'
     **moreFour = 46;
                ^

Только первое присваивание не сработало, потому что, если мы прочитаем наше объявление справа налево:
uint64_t const **moreFour = &aFour;

moreFour это:
  • указатель (*)
  • на указатель (*)
  • на неизменяемую переменную (uint64_t const)

Как мы видим, единственная операция, которую мы не смогли выполнить — это изменение хранимого значения. Мы успешно изменили указатель и указатель на указатель.

Два


Что, если мы хотим добавить еще один модификатор const на уровень глубже?
uint64_t const *const *evenMoreFour = &aFour;

Учитывая два const3, что мы теперь можем сделать?
**evenMoreFour = 46;
*evenMoreFour = NULL;
evenMoreFour = NULL;

clang-700.1.81:
error: read-only variable is not assignable
    **evenMoreFour = 46;
    ~~~~~~~~~~~~~~ ^
error: read-only variable is not assignable
    *evenMoreFour = NULL;
    ~~~~~~~~~~~~~ ^

gcc-5.3.0:
error: assignment of read-only location '**evenMoreFour'
     **evenMoreFour = 46;
                    ^
error: assignment of read-only location '*evenMoreFour'
     *evenMoreFour = NULL;
                   ^

Теперь мы дважды защищены от изменений, потому что, если мы прочитаем наше объявление справа налево:
uint64_t const *const *evenMoreFour = &aFour;

evenMoreFour это:
  • указатель (*)
  • на неизменяемый указатель (*const)
  • на неизменяемую переменную (uint64_t const)


Три


Мы можем сделать чуть лучше чем два. Знакомьтесь: три const.

Что если мы хотим заблокировать все изменения при объявлении двойного указателя?
uint64_t const *const *const ultimateFour = &aFour;

Что теперь мы (не)можем сделать?
**ultimateFour = 48;
*ultimateFour = NULL;
ultimateFour = NULL;

clang-700.1.81:
error: read-only variable is not assignable
    **ultimateFour = 46;
    ~~~~~~~~~~~~~~ ^
error: read-only variable is not assignable
    *ultimateFour = NULL;
    ~~~~~~~~~~~~~ ^
error: read-only variable is not assignable
    ultimateFour = NULL;
    ~~~~~~~~~~~~ ^

gcc-5.3.0:
error: assignment of read-only location '**ultimateFour'
     **ultimateFour = 46;
                    ^
error: assignment of read-only location '*ultimateFour'
     *ultimateFour = NULL;
                   ^
error: assignment of read-only variable 'ultimateFour'
     ultimateFour = NULL;
                  ^

Ничего не работает! Успех!

Поехали, ещё раз:
uint64_t const *const *const ultimateFour = &aFour;

ultimateFour это:
  • неизменяемый указатель (*const)
  • на неизменяемый указатель (*const)
  • на неизменяемую переменную (uint64_t const)

Дополнительные правила


  • Объявления const всегда безопасны (если вам не нужно изменять значения):
    • Любые не-const данные могут быть присвоены const переменной.
      Разрешено создание неизменяемых ссылок на изменяемые переменные:
      uint32_t abc = 123;
      uint32_t *thatAbc = &abc;
      uint32_t const *const immutableAbc = thatAbc;
      

    • Будьте осторожны и объявляйте столько const параметров функции, сколько можете
      void trySomething(const storageStruct *const storage,
            const uint8_t *const ourData,
            const size_t len) {
      saveData(storage, ourData, len);
      }
      


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



Хаки


Хаки приведения типов


Что если вы умны и создали изменяемый указатель на неизменяемое хранилище?
const uint32_t hello = 3;
uint32_t *getAroundHello = &hello;
*getAroundHello = 92;

Ваш компилятор будет жаловаться, что вы отбрасываете const, но просто выдавая предупреждение4, которое вы можете отключить5.
clang-700.1.81:
warning: initializing 'uint32_t *' (aka 'unsigned int *')
         with an expression of type 'const uint32_t *' (aka 'const unsigned int *')
         discards qualifiers [-Wincompatible-pointer-types-discards-qualifiers]
    uint32_t *getAroundHello = &hello;
              ^                ~~~~~~

gcc-5.3.0:
warning: initialization discards 'const' qualifier from pointer target type
         [-Wdiscarded-qualifiers]
     uint32_t *getAroundHello = &hello;
                                ^

Поскольку это C, вы можете отбросить квалификатор const явным преобразованием типа и избавиться от предупреждения (а также нарушения инициализации const):
uint32_t *getAroundHello = (uint32_t *)&hello;

Теперь у вас нет предупреждений при компиляции поскольку вы явно указали компилятору игнорировать настоящий тип &hello и использовать вместо него uint32_t *.

Хаки памяти


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

Давайте объявим две структуры, различающиеся только константностью их членов.
struct exampleA {
    int64_t a;
    uint64_t b;
};

struct exampleB {
    int64_t a;
    const uint64_t b;
}; 

const struct exampleA someStructA = {.a = 3, .b = 4};
struct exampleB someOtherStructB = {.a = 3, .b = 4}; 

Попробуем скопировать someOtherStructB в const someStructA.
memcpy(&someStructA, &someOtherStructB, sizeof(someStructA));

Будет ли это работать?
clang-700.1.81:
warning: passing 'const struct aStruct *' to parameter of type 'void *'
         discards qualifiers
         [-Wincompatible-pointer-types-discards-qualifiers]
    memcpy(&someStructA, &someOtherStructB, sizeof(someStructA));
           ^~~~~~~~~~~~

gcc-5.3.0:
In file included from /usr/include/string.h:186:0:
warning: passing argument 1 of '__builtin___memcpy_chk' discards 'const' qualifier
         from pointer target type [-Wdiscarded-qualifiers]
     memcpy(&someStructA, &someOtherStructB, sizeof(someStructA));
            ^
note: expected 'void *' but argument is of type 'const struct aStruct *'

Неа, это не работает, потому что прототип6 для memcpy выглядит так:
void *memcpy(void *restrict dst, const void *restrict src, size_t n);

memcpy не позволяет передавать ей неизменяемые указатели в качестве dst аргумента, так как dst изменяется при копировании (а someStructA неизменяема).

Хотя, проверка const параметров выполняется только прототипом функции. Будет ли жаловаться компилятор, если мы используем частично неизменяемую структуру с отдельными const полями в качестве dst?

Что произойдет, если мы попытаемся скопировать const someStructA в изменяемую, но содержащую один const член someOtherStructB?
memcpy(&someOtherStructB, &someStructA, sizeof(someOtherStructB));

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

Заключение


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

Попробуйте сами
#include <stddef.h> /* дает нам NULL */
#include <stdint.h> /* дает нам расширенные целочисленные типы */

int main(void) {
    uint64_t bob = 42;

    const uint64_t *aFour = &bob;
    /* uint64_t const *aFour = &bob; */

    *aFour = 44; /* НЕТ */
    aFour = NULL;

    const uint64_t *const anotherFour = &bob;
    /* uint64_t const *const anotherFour = &bob; */

    *anotherFour = 45; /* НЕТ */
    anotherFour = NULL; /* НЕТ */

    const uint64_t **moreFour = &aFour;
    /* uint64_t const **moreFour = &aFour; */

    **moreFour = 46; /* НЕТ */
    *moreFour = NULL;
    moreFour = NULL;

    const uint64_t *const *evenMoreFour = &aFour;
    /* uint64_t const *const *evenMoreFour = &aFour; */

    **evenMoreFour = 47; /* НЕТ */
    *evenMoreFour = NULL; /* НЕТ */
    evenMoreFour = NULL;

    const uint64_t *const *const ultimateFour = &aFour;
    /*  uint64_t const *const *const ultimateFour = &aFour; */

    **ultimateFour = 48; /* НЕТ */
    *ultimateFour = NULL; /* НЕТ */
    ultimateFour = NULL; /* НЕТ */

    return 0;
}






1 — это также означает, что можно абсолютно безопасно передавать const скаляры в функцию, использующую их как не-const параметры, так как она никак не может изменить исходные значения скалярных переменных.^

2 — в таких случаях может быть лучше написать uint64_t const * вместо const uint64_t *, поскольку оба этих объявления приводят в точности к одному и тому же результату, но читать ваше объявление справа налево становится удобней если квалификатор const следует за типом.^

3 — это также безусловно подтверждает, что правильный синтаксис для указателей это type *name, а не type* name и уж тем более не type * name потому что, когда мы добавляем const, указатель прикрепляется к следующему квалификатору, а не к предыдущему. Например:
Неправильно
uint64_t const* const* evenMoreFour; /* оба указателя прикреплены
                                        не к своим const */

Правильно
uint64_t const *const *evenMoreFour; /* const правильно читается
                                        справа налево. */
^

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

5 — напоминаю: const проверяется только во время компиляции; он не изменяет поведение программы, только если вы не ухитритесь нарушить ограничения накладываемые const (не больше, чем изменение любого другого значения изменило бы поведение вашей программы), но, вероятно, работать это будет не так как вы ожидаете. Также: ваш компилятор может разместить неизменяемые lданные в доступные только для чтения сегменты кода, и попытка обойти эти const-блоки может привести к неопределенному поведению.^

6 — также обратите внимание на ключевое слово restrict в прототипе memcpy(). restrict означает «данные этого указателя не пересекаются с другими данными в текущей области видимости», что определяет каким образом memcpy() планирует обрабатывать её параметры.
Если при копировании указатель на место назначения, частично перекрывает указатель на место откуда берутся данные, нужно использовать функцию memmove(), её прототип не содержит квалификаторов restrict.
void *memmove(void *dst, const void *src, size_t len);
^
Поделиться с друзьями
-->

Комментарии (57)


  1. mwizard
    28.05.2016 17:41
    +10

    Я удивлен, что в статье, претендующей на полноту, не рассматривается вариант с uint32_t hello[const] — эта конструкция объявляет константный указатель на массив. Элементы hello[] менять можно, а сам hello нет.


    1. DuDDiTs
      28.05.2016 17:54
      +1

      Спасибо, не знал о таком варианте.
      Только с ходу не могу придумать для него область применения. Но все равно интересно


      1. mwizard
        28.05.2016 18:07
        +9

        Область применения та же, что и у обычного константного указателя. Можно, например, объявлять глобальные константные массивы, компилятор тогда сможет закешировать указатель всюду, где этот массив используется.

        Еще из «внутрискобочных» объявлений есть foo[static 5], позволяет сказать компилятору, что в массиве есть не менее 5 элементов.


        1. DuDDiTs
          28.05.2016 18:10

          Спасибо, познавательно.


        1. segment
          28.05.2016 18:12

          Какой это стандарт?


          1. mwizard
            28.05.2016 18:13
            +6

            C99, параграф 6.7.6.3:

            A declaration of a parameter as ‘‘array of type’’ shall be adjusted to ‘‘qualified pointer to type’’, where the type qualifiers (if any) are those specified within the [ and ] of the array type derivation. If the keyword static also appears within the [ and ] of the array type derivation, then for each call to the function, the value of the corresponding actual argument shall provide access to the first element of an array with at least as many elements as specified by the size expression.


        1. jcmvbkbc
          28.05.2016 20:09
          +4

          Можно, например, объявлять глобальные константные массивы

          Нельзя. Эта конструкция может использоваться только как параметр в прототипе или определении функции.


    1. jcmvbkbc
      28.05.2016 20:14
      +4

      uint32_t hello[const] — эта конструкция объявляет константный указатель на массив.

      Эта конструкция объявляет массив, а не указатель, согласно c99 6.7.5.2:3.


  1. SilentBob
    28.05.2016 20:17
    +5

    Мне лень искать, это ошибка перевода, или оригинала, но

    const uint64_t *bob… это «неизменяемый указатель на неизменяемые данные».
    не верно, это всего лишь «изменяемый указатель на неизменяемые данные».


    1. DuDDiTs
      28.05.2016 20:26

      Спасибо, действительно ошибка перевода


  1. pftbest
    28.05.2016 20:45

    Когда я только начинал программировать на C, я думал что указание константности помогает компилятору генерировать более эффективный код. А, как оказалось на самом деле, константы это самые обычные переменные, и они имеют абсолютно одинаковые шансы быть оптимизированными на равне с другими переменными. Все проверки на константность происходят на уровне исходного кода и помогают только разработчику не делать глупых ошибок, а до оптимизатора эта информация не доходит.


    1. mbait
      28.05.2016 21:08

      Главная причина в том, что если тип — не указатель, то никаких оптимизаций и не применишь — чтение и запись происходят явно, а если указатель, то неизвестно, кто ещё ссылается на эту же область памяти (см. aliasing). В C99 добавили restrict, который помогает компилятору, но "мешает" пользователю в том плане, что пользователь должен гарантировать отсутствие алиасинга для данного указателя, иначе "я за себя не отвечаю, братцы".


      1. pftbest
        28.05.2016 21:32

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


        1. LynXzp
          28.05.2016 23:44

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


          1. grossws
            30.05.2016 17:51

            Это относится только к части контроллеров, ибо есть выше крыши с фон-неймановской архитектурой.


    1. khim
      29.05.2016 03:25
      +5

      И шо ви такое говорите. Разумеется объявление переменной как const может влиять на генерируемый код.

      Хотите посмотреть на суслика? Смотрите:
      $ cat test-int.c 
      extern int x;
      
      extern int foo(int);
      
      int bar() {
        int y = foo(x);
        return x + y;
      }
      $ gcc -O3 -S -o- test-int.c 
      	.file	"test-int.c"
      	.section	.text.unlikely,"ax",@progbits
      .LCOLDB0:
      	.text
      .LHOTB0:
      	.p2align 4,,15
      	.globl	bar
      	.type	bar, @function
      bar:
      .LFB0:
      	.cfi_startproc
      	subq	$8, %rsp
      	.cfi_def_cfa_offset 16
      	movl	x(%rip), %edi
      	call	foo
      	addl	x(%rip), %eax
      	addq	$8, %rsp
      	.cfi_def_cfa_offset 8
      	ret
      	.cfi_endproc
      .LFE0:
      	.size	bar, .-bar
      	.section	.text.unlikely
      .LCOLDE0:
      	.text
      .LHOTE0:
      	.ident	"GCC: (GNU) 5.3.0"
      	.section	.note.GNU-stack,"",@progbits
      $ cat test-constint.c 
      extern const int x;
      
      extern int foo(int);
      
      int bar() {
        int y = foo(x);
        return x + y;
      }
      $ gcc -O3 -S -o- test-constint.c 
      	.file	"test-constint.c"
      	.section	.text.unlikely,"ax",@progbits
      .LCOLDB0:
      	.text
      .LHOTB0:
      	.p2align 4,,15
      	.globl	bar
      	.type	bar, @function
      bar:
      .LFB0:
      	.cfi_startproc
      	pushq	%rbx
      	.cfi_def_cfa_offset 16
      	.cfi_offset 3, -16
      	movl	x(%rip), %ebx
      	movl	%ebx, %edi
      	call	foo
      	addl	%ebx, %eax
      	popq	%rbx
      	.cfi_def_cfa_offset 8
      	ret
      	.cfi_endproc
      .LFE0:
      	.size	bar, .-bar
      	.section	.text.unlikely
      .LCOLDE0:
      	.text
      .LHOTE0:
      	.ident	"GCC: (GNU) 5.3.0"
      	.section	.note.GNU-stack,"",@progbits
      


      1. pftbest
        29.05.2016 10:16
        +1

        Хм, и правда, полагается на UB. Был неправ, снимаю шляпу.


    1. Keroro
      29.05.2016 08:27
      +2

      В Гарвардской архитектуре (большинство микроконтроллеров), где память команд и данных физически раздельная, это просто необходимый спецификатор. Скажем, массив вида uint8_t a[255] будет помещён в RAM (а ее обычно весьма немного, и если на самом деле изменять данные в массиве не нужно, то это просто напрасная трата ресурсов). Массив же const uint8_t b[255] будет храниться только в ROM, не занимая лишних ресурсов. Та же история с указателями, от того, где в нем будет поставлен const, будет зависить, указатель ли это на переменную в ROM или RAM.


      1. khim
        29.05.2016 20:31

        Для этих целей лучше использовать __flash, нэ?

        Хотя использование const для этих целей — это вроде как традиция…


      1. LynXzp
        30.05.2016 13:58

        Смотря какой контроллер (и компилятор).

        Для avr (avr-gcc) const не разместит в flash, т.к. для чтения нужно будет обращаться не b[i], а pgm_read_byte(b[i]);


      1. LynXzp
        30.05.2016 14:04

        За то два одинаковых const массива/строки будут оптимизированны и займут одно место в памяти (какой бы ни было).


      1. grossws
        30.05.2016 18:34

        Не соглашусь только с


        В Гарвардской архитектуре (большинство микроконтроллеров)

        Сейчас большинство контроллеров с modified harvard, который является средним между чистым гарвардом и фон-нейманном (т. е. единое пространство памяти, но независимые кэши для данных и инструкций. Это относится к современным arm, avr32.


        Бывает более слабо модифицированный гарвард представлен у avr и pic, где адресные пространства разные, но при этом есть инструкции для чтения и модификации памяти инструкций.


        Ещё бывают 8051/8052 с чистой фон-нейманновской архитектурой (в частности, при использовании внешней памяти), arm7, msp430.


  1. andy_p
    28.05.2016 22:44
    +1

    Кстати да, интересный вопрос — если ставить const в параметры функции

    void foo(const int a, const double b)

    влияет ли это на оптимизацию?
    Я слышал разные мнения.


    1. FoxCanFly
      29.05.2016 01:21

      implementation defined же. Стандартом не регламентируется это


    1. torkve
      29.05.2016 09:39

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


      1. andy_p
        29.05.2016 16:05

        Чем такая оптимизация отличается от оптимизации const T * const?


        1. torkve
          29.05.2016 16:59

          Тем, что можно, например, вызвать код вида

          void do_some_job(const T& x) { ... }
          
          do_some_job(T());
          

          без боязни утечки или накладных расходов на создание/дестрой умного указателя.


  1. Mingun
    29.05.2016 10:40

    А мне вот интересно, регламентируется ли как-то стандартом размещение в памяти частично неизменяемых структур? Может ли оптимизатор выкидывать неименяемые члены, заменяя их на константы? И если нет, зачем вообще в практическом плане такие структуры могут потребоваться?


    1. CrashLogger
      29.05.2016 11:03

      Например сигнатуры в заголовках файлов — MZ, PE и тому подобное. Выкидывать нельзя, т.к. при записи структуры в файл это поле там должно быть.


    1. metopa
      29.05.2016 11:39

      Не может. Они хоть и неизменяемые, но у каждой структуры значения разные. Поэтому в компайл-тайме о них ничего не известно. Возможно, применяются такие же оптимизации, как и с обычным const.
      Если имеется в виду одна и та же константа, разделённая между всеми структурами, то пишут struct {static const int c;}, но это вроде как C++ only


      1. Mingun
        30.05.2016 17:04

        Ну как же неизвестно. При создании структуры нам же по любому инициализировать эти поля придётся, даже если в разных структурах значения констант будут разными, они же всё равно будут известны компилятору. А после инициализации изменить их уже нельзя.


        1. khim
          30.05.2016 22:37

          Структуры, вообще-то, можно передавать из одной части программы в другую. То, что в некоторых случаях компилятор может понять — какой значение будет у поля не отменяет того факта, что бывают случаи, когда он этого сделать не может — а формат структуры, который может в одном месте быть одним, а в другом месте — другим стандарт не предусматривает (смотрим сюда).

          P.S. Интересно — есть ли какой-нибудь подобный сайт посвящённый именно языку C, а не C++? Всё-таки это несколько разные языки (хотя вот конкретно эта часть одинакова).


          1. Mingun
            31.05.2016 18:03

            Так в том то и дело, что в данной ситуации компилятор может понять, какой формат структуры во всех случаях использования этой самой структуры :) Он может, например, выкидывать константные поля и offsetof будет просто возвращать смещение, как будто бы этих полей в принципе в структуре нет. Если в стандарте не закреплено определённое размещение структуры с константными полями, кто ему мешает проводить такую оптимизацию? Вот мне бы и хотелось услышать от знающих людей, что по этому поводу говорит стандарт.


            1. khim
              31.05.2016 22:29
              +1

              Так в том то и дело, что в данной ситуации компилятор может понять, какой формат структуры во всех случаях использования этой самой структуры :)
              Это, я извиняюсь, как?

              Рассмотрим простейший пример.

              Библитека:
              struct Serializer {
                const int version;
                int offset;
                int counter;
                ...
              };
              
              int Serialize(struct Serializer* serializer) {
                ... используем serializer->version ...
              }


              Программа:
              struct Serializer g_serializer = { 10 };
              ...
                Serialize(&g_serializer)
              ...
              


              Как компилятор может при сборке библиотеки догадаться что будет в поле version, если главная программа ещё даже не спроектирована в момент сборки это библиотеки? Хрустальным шаром, позволяющим предсказывать будущее, компьютеры пока не комплектуются…

              Вот мне бы и хотелось услышать от знающих людей, что по этому поводу говорит стандарт.
              А чего он, собственно, может сказать? Константные поля — это такие же поля, как и любые другие. Указатель на них можно передать куда-нибудь, где вообще не будет известно, что это поля структуры. Так что в общем случае ничего с этим поделать нельзя.

              В частном случае — можно: увидев, что, скажем, структура «за пределы функции» не выходит компилятор может её вообще извести, оставив от неё отдельные поля — но это от константности не зависит: в точности то же самое компилятор и с обычной структурой может проделать.


              1. Mingun
                01.06.2016 17:44

                О! Это именно то, что я хотел увидеть. Я просто не учёл, что в библиотеке структура вообще может никогда не создаваться, а использоваться только через указатель.


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


  1. socketpair
    29.05.2016 14:12

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

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


    1. ZyXI
      29.05.2016 18:11
      +1

      По?моему, когда нет взятия адреса и переменная явно не изменяется, компилятор может это делать и без const. А при объявлении массива можно использовать любые переменные, просто это будет аналогом вызова alloca:


      /// @file test.c
      #define U __attribute__((unused))
      int main(int argc U, char **argv U, char **environ U)
      {
        const int x = 5;
        char v[x] U;
        return 0;
      }

      % clang -pedantic -std=c99 -Wall -Wextra -Weverything test.c
      test.c:6:9: warning: variable length array used [-Wvla]
        char v[x] U;
              ^
      1 warning generated.

      Видите: написано const, а компилятор считает, что у нас VLA (variable length array). И предупреждение не изменится, если написать static const int x = 5;. В том числе, если переместить определение x перед функцией, а не внутри неё. gcc считает также, только нужно заменить -Weverything на -Wvla, т.к. это clang?специфичная возможность, а -Wvla ни в -Wall, ни в -Wextra не входят.


      Если что, для VLA можно использовать любые целочисленные выражения:


      /// @file test.c
      #include <stdio.h>
      #define U __attribute__((unused))
      int main(int argc, char **argv U, char **environ U)
      {
        char v[printf("%x", argc)] U;
        return 0;
      }

      тоже работает, хотя printf() выдаёт ни разу не константу и вообще результат printf здесь не вычислим на этапе компиляции.


      1. ZyXI
        29.05.2016 18:21

        Если что, VLA, как и alloca, относятся к возможностям, которые использовать запрещено во многих проектах. К примеру, Google. Основная претензия: шансы получить stack overflow куда выше, и вы будете получать SEGV (в лучшем случае) вместо NULL на выходе функции; alloca приемлем для малых объёмов памяти, но куда как проще забанить alloca и VLA полностью, чем проконтролировать, что программист не будет злоупотреблять этими возможностями.


  1. Randl
    29.05.2016 18:47

    А constexpr оптимизируется лучше чем const?


    1. Antervis
      29.05.2016 19:04

      constexpr во-первых является конструкцией с++, а не си, а во-вторых, несет несколько иной смысл. Вместо constexpr функции/переменной компилятор сразу подставляет посчитанное на этапе компиляции значение (если его можно посчитать на этапе компиляции). А const просто означает, что переменную нельзя поменять.


      1. Randl
        29.05.2016 19:15

        Я понимаю. Вопрос, есть ли смысл (и стоит ли) менять const на constexpr, везде, где возможно или нет?


        1. khim
          29.05.2016 20:46

          А это смотря чего вы хотите достичь :-)

          Правда, грубо говоря, следующая: если замена const на constexpr возможна — то пофиг, что использовать, на генерируемый код это не повляет.

          Однако есть места, где можно использовать только const (например если ваша константа зависит от параметра функции) и есть места, где можно использовать только constexpr (например если вы хотите параметризовать этой переменной шаблон).

          Менять ли… я бы сказал, что да, стоит — но без ажиотажа. Выгода не для компилятора, а для программиста: в тех местах где для const будет сгенерирован неэффективный код constexpr выдаст ошибку, что позволит её оперативно заметить и исправить, что, несомненно, полезно, но не настолько, чтобы прям всё бросить и бросаться менять все const в большом проекте…


          1. Randl
            29.05.2016 21:10

            Правда, грубо говоря, следующая: если замена const на constexpr возможна — то пофиг, что использовать, на генерируемый код это не повляет.

            Ну я так и думал если честно.


            Менять ли… я бы сказал, что да, стоит — но без ажиотажа.

            Я сам вставляю constexpr вместо const и uint32_t вместо int и т.д. Но в чужом коде это редко встречаю — думал может упускаю что-то.


            1. Antervis
              30.05.2016 06:31

              тогда уж заменяй на uint_fast32_t.
              И не забывай проверять constexpr через static_assert. А то маловато смысла писать constexpr там, где на вход функции подается не-constexpr значение


              1. Randl
                30.05.2016 12:50

                Имелось ввиду семейство типов, используется иногда fast, иногда least.


              1. khim
                30.05.2016 12:54

                И не забывай проверять constexpr через static_assert. А то маловато смысла писать constexpr там, где на вход функции подается не-constexpr значение.
                Какой нафиг «constexpr там, где на вход функции» что-то подаётся? Аргументы функции могут быть const, но не constexpr, а потому вместо static_assert приходится throw писать…


                1. Antervis
                  31.05.2016 10:00

                  constexpr не является жестким требованием для функции. Подадим на вход compile-time значение и она посчитается на этапе компиляции. Подадим на вход runtime значение и получим обычную функцию. В сложных сценариях работы компилятор может не понять, что на самом деле на входе функции константа, и посчитать её runtime-значением. Отличить один сценарий от второго можно либо просматривая asm-выхлоп, либо пользуясь static-assert'ом


                  1. khim
                    31.05.2016 16:36

                    Отличить один сценарий от второго можно либо просматривая asm-выхлоп, либо пользуясь static-assert'ом
                    В том-то и дело, что никакого static_assert'а вам не положено!

                    Так:
                    constexpr int foo(const int x) {
                      static_assert(x > 0, "x must be positive!");
                      return 2 * x;
                    }
                    не работает
                    $ g++ -std=c++14 -c test1.cc 
                    test1.cc: In function 'constexpr int foo(int)':
                    test1.cc:2:3: error: non-constant condition for static assertion
                       static_assert(x > 0, "x must be positive!");
                       ^
                    test1.cc:2:3: error: 'x' is not a constant expression
                    


                    1. Antervis
                      31.05.2016 18:57

                      вы меня совершенно не поняли, я про совершенно другой сценарий. static_assert'ом можно проверить, считается результат constexpr функции на этапе компиляции или же нет.


                      1. khim
                        31.05.2016 22:34

                        А… Понял. Это да. К сожалению это всё можно сделать только снаружи. А внутри — никаких constexpr и потому никаких static_assert… а хочется… частенько хочется.


  1. mcBottle
    29.05.2016 22:53

    как я понимаю:
    unit32_t const *const *a;
    unit32_t *b = *a;
    b = 42;
    вызовет ошибку еще на 2 строке
    А вот это:
    unit32_t const *const *a;
    unit32_t *b =(unit32_t) *a;
    b = 42;


  1. AxisPod
    30.05.2016 11:18

    Из простого сделать статью таких размеров, аааа, слов нет. Да и не заметил самого простого объяснения. Если const до */& — относится к значению, после — к указателю.


    1. DuDDiTs
      30.05.2016 11:28

      Статья большая, потому что в ней описывается применение const не только к указателям.

      Ну и если я вас правильно понял, то о чем вы говорите описывается в разделе «Интерлюдия — объясняем объявления const»


  1. amarao
    30.05.2016 11:27
    +1

    Фраза uint64_t const *const *const ultimateFour = &aFour; у меня вызывает смех и воспоминания про анекдот "… ничего не трогай и покорми собаку".


  1. nickolaym
    31.05.2016 13:33

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


    Нет.

    void foo(int* a);
    void bar(int* const b);
    

    В обоих случаях тип функции — void(int*), именно по той самой причине: указатель нельзя поменять изнутри наружу.

    А вот указуемое — тут правда
    void foo(int* a);
    void buz(int const* b); // void (int const*)
    


    1. DuDDiTs
      31.05.2016 13:56

      Тогда скорее не просто «Нет.», а только в некоторых случаях может не ругаться.

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


      1. nickolaym
        31.05.2016 14:53

        Не «в некоторых случаях может не ругаться», а что считать параметром. Параметр-указатель — или параметр — косвенные данные?
        Так что это или корявый перевод, или корявое изложение мысли автором.

        Вообще, мотив всей статьи можно было свернуть в одну фразу: «не путайте константность указателя и константность указуемого».
        С примечанием «компилятор игнорирует константность аргументов функции».
        И со вторым примечанием «компилятор игнорирует в типе функции, но не в её теле»

        typedef struct { int x; } foo;
        
        void f(const foo t) { t.x = 1; } /* ошибка, неконстантный доступ к константной переменной */
        void g(foo t) { t.x = 1; }
        
        void (*p)(foo) = f; /* ошибки нет, сигнатуры f и g одинаковы */