Так ви думаєте, що знаєте Const?

Від перекладача:
Пропоную вам переклад пости з блогу Метта Стэнклиффа (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};

Або:
const struct 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;

Враховуючи два
const
3, що ми тепер можемо зробити?
**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 of argument 1 '__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);
^
Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.