Pull to refresh

Comments 57

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

Еще из «внутрискобочных» объявлений есть foo[static 5], позволяет сказать компилятору, что в массиве есть не менее 5 элементов.
Спасибо, познавательно.
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.
Можно, например, объявлять глобальные константные массивы

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

Эта конструкция объявляет массив, а не указатель, согласно c99 6.7.5.2:3.
Мне лень искать, это ошибка перевода, или оригинала, но
const uint64_t *bob… это «неизменяемый указатель на неизменяемые данные».
не верно, это всего лишь «изменяемый указатель на неизменяемые данные».
Спасибо, действительно ошибка перевода
Когда я только начинал программировать на C, я думал что указание константности помогает компилятору генерировать более эффективный код. А, как оказалось на самом деле, константы это самые обычные переменные, и они имеют абсолютно одинаковые шансы быть оптимизированными на равне с другими переменными. Все проверки на константность происходят на уровне исходного кода и помогают только разработчику не делать глупых ошибок, а до оптимизатора эта информация не доходит.

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

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

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

И шо ви такое говорите. Разумеется объявление переменной как 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


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

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

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

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


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

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


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


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

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

void foo(const int a, const double b)

влияет ли это на оптимизацию?
Я слышал разные мнения.
В большинстве компиляторов есть, например, оптимизация памяти в некоторых случаях, если аргументом функции является const T&.
Тем, что можно, например, вызвать код вида
void do_some_job(const T& x) { ... }

do_some_job(T());

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

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

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

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

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

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

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

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

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

Библитека:
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, если главная программа ещё даже не спроектирована в момент сборки это библиотеки? Хрустальным шаром, позволяющим предсказывать будущее, компьютеры пока не комплектуются…

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

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

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


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

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

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

По‐моему, когда нет взятия адреса и переменная явно не изменяется, компилятор может это делать и без 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 здесь не вычислим на этапе компиляции.

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

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

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

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

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

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

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

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

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


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

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

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

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

И не забывай проверять constexpr через static_assert. А то маловато смысла писать constexpr там, где на вход функции подается не-constexpr значение.
Какой нафиг «constexpr там, где на вход функции» что-то подаётся? Аргументы функции могут быть const, но не constexpr, а потому вместо static_assert приходится throw писать…
constexpr не является жестким требованием для функции. Подадим на вход compile-time значение и она посчитается на этапе компиляции. Подадим на вход runtime значение и получим обычную функцию. В сложных сценариях работы компилятор может не понять, что на самом деле на входе функции константа, и посчитать её runtime-значением. Отличить один сценарий от второго можно либо просматривая asm-выхлоп, либо пользуясь static-assert'ом
Отличить один сценарий от второго можно либо просматривая 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

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

А вот так:
#include <stdexcept>

constexpr int foo(const int x) noexcept {
  return x < 0 ? throw std::logic_error("x must be positive!") :
                 2 * x;
}

constexpr int i = foo(1);
#ifdef TRY_NEGATIVE
constexpr int j = foo(-1);
#endif
работает
$ g++ -std=c++11 -c test3.cc 
$ clang++ -std=c++11 -c test3.cc
Как видим всё скомпилировалось.

$ g++ -std=c++11 -c -DTRY_NEGATIVE test3.cc 
test3.cc:10:22:   in constexpr expansion of 'foo(-1)'
test3.cc:4:62: error: expression '<throw-expression>' is not a constant-expression
   return x < 0 ? throw std::logic_error("x must be positive!") :
                                                              ^
$ clang++ -std=c++11 -c -DTRY_NEGATIVE test3.cc
test3.cc:10:15: error: constexpr variable 'j' must be initialized by a constant expression
constexpr int j = foo(-1);
              ^   ~~~~~~~
test3.cc:4:18: note: subexpression not valid in a constant expression
  return x < 0 ? throw std::logic_error("x must be positive!") :
                 ^
test3.cc:10:19: note: in call to 'foo(-1)'
constexpr int j = foo(-1);
                  ^
1 error generated.
А тут — не скомпилировлаось.
вы меня совершенно не поняли, я про совершенно другой сценарий. static_assert'ом можно проверить, считается результат constexpr функции на этапе компиляции или же нет.
А… Понял. Это да. К сожалению это всё можно сделать только снаружи. А внутри — никаких constexpr и потому никаких static_assert… а хочется… частенько хочется.
как я понимаю:
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;
Из простого сделать статью таких размеров, аааа, слов нет. Да и не заметил самого простого объяснения. Если const до */& — относится к значению, после — к указателю.
Статья большая, потому что в ней описывается применение const не только к указателям.

Ну и если я вас правильно понял, то о чем вы говорите описывается в разделе «Интерлюдия — объясняем объявления const»
Фраза uint64_t const *const *const ultimateFour = &aFour; у меня вызывает смех и воспоминания про анекдот "… ничего не трогай и покорми собаку".
Однако, ваш компилятор будет жаловаться на несоответствие const для параметров, являющихся указателями или массивами, так как в таком случае ваша функция будет иметь возможность манипулировать данными на которые ссылается передаваемый указатель.


Нет.

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

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

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

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

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

Вообще, мотив всей статьи можно было свернуть в одну фразу: «не путайте константность указателя и константность указуемого».
С примечанием «компилятор игнорирует константность аргументов функции».
И со вторым примечанием «компилятор игнорирует в типе функции, но не в её теле»
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 одинаковы */

Sign up to leave a comment.

Articles