- PVSM.RU - https://www.pvsm.ru -
От переводчика:
Предлагаю вам перевод поста из блога Мэтта Стэнклиффа [1] (Matt Stancliff), автора нашумевшей на хабре статьи Советы о том, как писать на С в 2016 году [2].
Здесь Мэтт делится знаниями о квалификаторе типа 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 [3].
Однако, ваш компилятор будет жаловаться на несоответствие 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
для отдельных членов структуры:
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
для указателей — вот где начинается веселье.
Давайте использовать указатель на целое число в качестве примера.
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
и посмотрим как пойдут дела.
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
?
Значение тут кажется менее очевидным.
Значение настолько шатко, потому что на самом деле рекомендуется читать объявления переменных справа налево [4] (или ещё хуже, спиралью [5]).
В данном случае, если читать справа налево2 [6], это объявление означает:
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
и позволить изменять и сам указатель и данные на которые он указывает
uint64_t *bob;
uint64_t const *bob;
Это распространенный шаблон для перебора последовательностей данных: переходить к следующему элементу, увеличивая указатель, но не позволяя указателю изменять данные.
uint64_t *const bob;
Допустимое значение указателя это всегда скалярный адрес памяти (uintptr_t
), поэтому здесь const
оказывает тот же эффект, как и в случае с обычными целочисленными значениями, т.е. совершенно нормально, если ваша реализация использует const
для определения параметров, но прототип вашей функции не обязан включать их [7], так как этот const
защищает только адрес, но не данные.
uint64_t const *const bob;
Это то, что касается одного указателя и двух 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;
Учитывая два const
3 [8], что мы теперь можем сделать?
**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 [9]5 [10].
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 [11] для 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;
}
const
скаляры в функцию, использующую их как не-const
параметры, так как она никак не может изменить исходные значения скалярных переменных.^ [12]uint64_t const *
вместо const uint64_t *
, поскольку оба этих объявления приводят в точности к одному и тому же результату, но читать ваше объявление справа налево становится удобней если квалификатор const
следует за типом.^ [13]type *name
, а не type* name
и уж тем более не type * name
потому что, когда мы добавляем const
, указатель прикрепляется к следующему квалификатору, а не к предыдущему. Например:uint64_t const* const* evenMoreFour; /* оба указателя прикреплены
не к своим const */
Правильно
uint64_t const *const *evenMoreFour; /* const правильно читается
справа налево. */
^ [14]
4 — ну, нужно будет использовать нестандартизированный флаг в зависимости от модели компилятора, поэтому процесс сборки может потребовать много избыточных флагов для совместимости с различными компиляторами, чтобы отключить эти предупреждения.^ [15]
5 — напоминаю: const
проверяется только во время компиляции; он не изменяет поведение программы, только если вы не ухитритесь нарушить ограничения накладываемые const
(не больше, чем изменение любого другого значения изменило бы поведение вашей программы), но, вероятно, работать это будет не так как вы ожидаете. Также: ваш компилятор может разместить неизменяемые lданные в доступные только для чтения сегменты кода, и попытка обойти эти const
-блоки может привести к неопределенному поведению.^ [16]
6 — также обратите внимание на ключевое слово restrict
в прототипе memcpy()
. restrict
означает «данные этого указателя не пересекаются с другими данными в текущей области видимости», что определяет каким образом memcpy()
планирует обрабатывать её параметры.
Если при копировании указатель на место назначения, частично перекрывает указатель на место откуда берутся данные, нужно использовать функцию memmove()
, её прототип не содержит квалификаторов restrict
.
void *memmove(void *dst, const void *src, size_t len);
^ [17]
Автор: DuDDiTs
Источник [18]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/programmirovanie/123272
Ссылки в тексте:
[1] Мэтта Стэнклиффа: https://twitter.com/mattsta
[2] Советы о том, как писать на С в 2016 году: https://habrahabr.ru/company/inoventica/blog/275685/
[3] 1: #n1
[4] читать объявления переменных справа налево: https://stackoverflow.com/questions/1143262/what-is-the-difference-between-const-int-const-int-const-and-int-const
[5] спиралью: http://c-faq.com/decl/spiral.anderson.html
[6] 2: #n2
[7] прототип вашей функции не обязан включать их: #prototype
[8] 3: #n3
[9] 4: #n4
[10] 5: #n5
[11] 6: #n6
[12] ^: #n1-lnk
[13] ^: #n2-lnk
[14] ^: #n3-lnk
[15] ^: #n4-lnk
[16] ^: #n5-lnk
[17] ^: #n6-lnk
[18] Источник: https://habrahabr.ru/post/301332/?utm_source=habrahabr&utm_medium=rss&utm_campaign=best
Нажмите здесь для печати.