Комментарии 45
Неплохая затея, но быть может стоит подобные списки разбивать на "нужные" и "забавные" - и чуть ли не отдельными постами предлагать :) Всё-таки он уж очень длинным получился.
так написано же: фичи, фокусы и причуды языка C
условно полезными могут быть только фичи как я понимаю, но и они должны уступать место штатным возможностям если нет какой то непреодолимой необходимости использовать именно фичу в конкретном случае. Вы же не будете строить свое решение на фичах, фокусах, и причудах? Код вроде как должен быть понятным, а не причудливо-волшебным.
Сращивания строк или о вреде комментариев.
Как то еще очень давно был у меня на С вот такой кусок кода:
if(disk == 0)
Что то делаем только для корневого каталога диска С
Что то делаем всегда
И вот решил я добавить комментарий
if(disk == 0) // Обращаемся к C:\
Что то делаем только для корневого каталога диска С
Что то делаем всегда
Долго не мог понять почему по условию вместо выполнения кода в строке 2, выполняется код в строке 3. И да, раскраску кода в редакторе, тогда еще не завезли.
На самом деле это о вреде опускания фигурных скобок. А были бы скобки, компилятор вам бы сказал, что закрывающих больше
Тут больше о проблеме использования "магических" констант и необходимости коментария там, где этого можно избежать заменив "0" на дефайн или енум с описательным именем.
Не факт. Если вы пишете со скобками так:
if (1) // \
{
code();
}
то да, но вот так:
if (1) { // \
code();
}
компилятор вполне проглотит if (1) {}
. Следующее выражение в этом случае не станет условным, как в случае совсем без скобок, но вот, например, echo 'int main(void) { if (1) {} }' | clang -std=c99 -pedantic -Wall -Weverything -xc -
даже не производит никаких предупреждений.
это лишь говорит о каких--то условностях устоявшихся для языка и, на мой взгляд, застрявших в прошлом. Берем C# и пишем
int a = 1;
if (a == 1) // комментарий
Console.WriteLine(a);
Console.WriteLine(--a);
на выходе получаем
1
0
что логично.
Вы видимо не поняли в чём прикол.
Символ \ в конце строки означает продолжение выражения на следующей строке. В том числе комментария. Таким образом, комментарий "обращаемся к C:\" приводит к тому, что следующая строка также является комментарием, а не кодом.
Вычисление sizeof во время компиляции с возникновением сопутствующей ошибки duplicate case
Способ покороче =)
char (*__this_will_fail__)[sizeof( YourTypeHere )] = 1;
main.cpp:10:8: error: cannot initialize a variable of type 'char (*)[8]' with an rvalue of type 'int'
Диграфы, триграфы и альтернативные токены
Мы всё дальше и дальше от бога...
Битовые поля ... Вот как будет выглядеть в памяти вышеприведённый код
Упаковка битовых полей в памяти полностью зависит от реализации. Вот из вики, например:
The layout of bit fields in a C
struct
is implementation-defined. For behavior that remains predictable across compilers, it may be preferable to emulate bit fields with a primitive and bit operators:
idx[arr]
А оно не кастанётся к типу индекса?
Отрицательные индексы массива
Лучше не надо. Обращение за границами массива — это UB
Упаковка битовых полей в памяти полностью зависит от реализации.
Большая часть "implementaion-defined" в классическом Си - подстраховка на случай всяких экзотических архитектур и реализаций... На обычных процессорах всё однозначно и можно забить посмотреть в мануале к компилятору и спокойно юзать.
А оно не кастанётся к типу индекса?
a[b] это абсолютный аналог *(a+b). Просто синтаксический сахар. От перестановки мест слагаемых ничего не меняется.
Лучше не надо. Обращение за границами массива — это UB
С чего это? Типичный способ например обрезать пробелы в конце строки -
char *p = buf + strlen(buf);
while((p > buf) && isspace(p[-1]))
p--;
*p = 0;
Никаких уб
a[b] это абсолютный аналог *(a+b). Просто синтаксический сахар. От перестановки мест слагаемых ничего не меняется.
То есть int + void* приведётся к тому же типу, что и void* + int? Сомнительно что-то
С чего это?
Первая часть не понятна. a[b] это *(a+b), не *(b+a). Что тут void*, что int и почему они должны местами поменяться?
Вобще похоже приводится к void* оба, но это странно:
void *a = malloc(20);
int b = 10;
size_t c = b + a, d = a + b;
check.c:8:20: warning: initialization of ‘size_t’ {aka ‘long unsigned int’} from ‘void *’ makes integer from pointer without a cast [-Wint-conversion]
8 | size_t c = b + a, d = a+b;
| ^
check.c:8:31: warning: initialization of ‘size_t’ {aka ‘long unsigned int’} from ‘void *’ makes integer from pointer without a cast [-Wint-conversion]
8 | size_t c = b + a, d = a+b;
| ^
Только void* не слишком удобно. Но похоже Вы еще один пункт для статьи нашли:
int a[4] = {0,1,2,3};
int b = 2;
printf("%d %d",*(a+b),*(b+a));
"2 2", никаких ворнингов. Никогда бы не подумал.
Про индекс массива: https://wiki.sei.cmu.edu/confluence/display/c/ARR30-C.+Do+not+form+or+use+out-of-bounds+pointers+or+array+subscripts - про это? Напоминает "Нельзя курить во время тренировки но можно тренироваться во время курения". А если массив без размера? А если массив не был объявлен как массив? А если по каким то соображениям было удобно B определить как &A[n], то B[-1] уже нельзя, но A[n-1] можно? А если...? Так то проблема серьезная, но формализация метода решения странная и все игнорят (и я тоже, увы).
А разве void
имеет известный размер, чтобы участвовать в адресной арифметике?
a[b] это *(a+b), не *(b+a). Что тут void*, что int и почему они должны местами поменяться?
Речь про то, что нет разницы между (a + b) и (b + a) при кастовании типов, если a — это указатель, а b — целое. Я до сегодня думал, что типы кастуются слева направо. И в вышеприведённых примерах у выражения будет типы type(a) и type(b), в первом и втором случаях, соответственно. Оказалось, что там есть сложная система.
Про сабскрипт
На хабре же была отличная статья с тестами про чтение за границами массива. Да я и сам, не так давно, переезжал с uclibc на musl и тоже обнаружил там и сям пару сегфолтов.
Я до сегодня думал, что типы кастуются слева направо
та же беда.
На хабре же была отличная статья с тестами про чтение за границами массива
Статью прочитал но прямой связи не увидел. Если я объявил что-то как char [16 (константа)] и потом полез к 17му байту (константа) то да, разворот цикла поломается. Наверное. По крайней мере приводится случай когда он поломался. Если я объявил что-то как char *, присвоил этому адрес из середины другого куска (важно что не выделял) и начал прыгать по нему как по массиву (так более читаемо, и выше много примеров что так думаю не я один) - то, возможно, компилятор который на этом сломается я не встречу пока сам не напишу, т.к. массовым ему не стать.
UB - это обращение к памяти по указателю за пределами распределённой памяти. А получение оперирование адресами такой памяти UB не является. Иначе само существование такой сущности, как нулевой указатель, было бы UB.
А получение оперирование адресами такой памяти UB не является
Получение не является, разыменование — является
как бы да, но есть нюансы :)
https://man7.org/linux/man-pages/man2/userfaultfd.2.html
За что минус то. Два треда. Один начал что-то читать но подвис. Второй тем временем все переписал. Первый очнулся, прочитал до конца, в том числе ссылку, которая уже не ссылка. Если хочется локфри - а его хочется, то можно или как-то руками проверить - а ссылка ли это, или перехватить сигсегв или вот эту штуку использовать. Вполне себе такой штатный сигсегв получается.
Теоретически это UB. Практически для ситуации описанной выше (там не код а данные, которые мы не пишем а читаем), не такое уж оно и undefined.
Мы можем прочитать мусор, позже это опознаем и выкинем.
Мы можем зациклиться если это "преследование указателя". Вариант теоретический, но надо иногда проверять а не читаем ли мы мусор до того как указатель догоним.
Мы можем упасть в сегфолт. Самое неприятное. Можно проверить, действительно ли эту память выделяли. Дорого, ситуация редкая а проверять придется постоянно. Можем перехватить сегфолт, проверить а не в определенной ли точке кода он случился, и если там то longjmp на выход, а если нет то валимся дальше. Ну или в свежем линуксе сделать тоже самое более цивилизованным способом (я кстати все не соберусь попробовать).
Британскую королеву код славить точно не начнет. В результате получается усточивость к манипуляциям ядра с процессом и реализуемость некоторых локфри алгоритмов в юзерспейс. Обычно такой трюк для подкачки используется и возможные адреса обращений там все таки резервируются, тут извращенное применение но работает.
Мне интересно, что скажет на это компилятор с -О3... если это уб, то он может просто выпилить.
C точки зрения компилятора вообще нет причин беспокоиться. Упрощенно как-то так выглядит:
typedef struct someStructTg {
struct someStructTg *next;
int key;
int value;
} someStruct;
extern jmp_buf recover;
int get_data(volatile someStruct *s, int key)
{
if (!s) return -1;
int wcnt = get_writes_counter();
volatile someStruct *p;
// Тут где-то cpu мы потеряли, кто-то поломал цепочку, cpu нам вернули.
// Блокировку от этого события мы сознательно пропустили
if (setjmp(recover))
return -1; // Сегфолтнулись ниже
while (key != s->key && (p = s->next))
s = p;
int rv = s->value;
// Тут барьер чтобы rv не выпилили но допустим он в get_writes_counter
if (get_writes_counter() != wcnt)
return -1; //проверяет что никто ничего не писал пока читали
return rv;
}
Что-то как-то сумбурно получилось
while (key != s->key && (p = s->next))
s = p;
можно читать как
while (key != s->key && (s = s->next));
if (!s) return -1;
хотя что именно возвращает функция неважно для иллюстрации, но так что-то более осмысленное. Туманная ремарка про потерю и возврат CPU, потому что это про RCU, и мы под грейс-периодом, который в случае разных процессов а не тредов злое ядро может неограниченно продлить.
https://github.com/runityru/rc-singularity/blob/9d1789fdf8754615195a111f2279f71972334e04/allocator.c#L92 тут есть рабочая реализация старой версии без темной магии с сегфолтами, с проверкой. Которую приходится при каждом разыменовании в цепочке выполнять, а можно оптом через сегфолт или userfaultfd. Но идея та же.
Похоже фразой про нюансы я случайно задел чьи то чувства. Может все таки поясните свою позицию? Какое из следующих утверждений является неверным?
макроассемблерЯВУ Си не имеет встроенных средств отличия "распределенной" памяти от "нераспределенной".Он полагается в различении этих категорий в масштабах страниц на железо и ОС
В масштабах байт статус "распределенности" не играет никакой роли, важно "назначение". Использование памяти "не по назначению" приведет к UB, даже если она перед этим была "распределена" маллоком, на стеке или еще как-то. За "назначение" отвечают компилятор, линковщик, загрузчик процесса, процессор - за свои конечные подмножества случаев, программист - за все остальное.
Если ОС говорит, что страница данного адреса была выделена процессу, и адрес либо назначен в конечные подмножества пункта 3, но его использование не противоречит определенным правилам, установленным языком (вызов функции, разыменование переменной, чтение данных), либо не назначен в них и правила использования памяти, произвольно установленные программистом, не противоречат сами себе и правилам выше, то с точки зрения языка адрес "распределен", даже если явно никогда не выделялся. При этом способы, которыми ОС и программист выполняют свою часть работы, никак языком не регулируются, кроме нескольких частных случаев, об одном из которых дискуссия выше.
Из пунктов 2-4 следует что чтение данных по произвольному адресу не является UB пока ОС и железо не против, чего можно добиться способами, к языку не относящимися.
Не претендую на истину в последней инстанции, но хотелось бы серьезных возражений.
https://alf.nu/EvilC в тему
А я подумал о том, что они только только убрали из языка всякие not, or, а в другие языки их стали завозить, вмести со всякими сопоставлениями с шаблонами.
Именованные параметры зацепили, остаётся, правда, вопрос, не мешает ли это оптимизации? Так-то параметры могут быть в регистрах, а упакованные в структуру уже не факт.
Структуры тоже передаются через регистры, если размер поля не больше 8 байт:
3.2.3 Parameter Passing
The classification of aggregate (structures and arrays) and union types works as follows:
4. Each field of an object is classified recursively
https://gitlab.com/x86-psABIs/x86-64-ABI/-/jobs/artifacts/master/raw/x86-64-ABI/abi.pdf?job=build
А как быть с архитектурами, где из регистров только аккумулятор на 8 бит?
Да, 8 байт это размер 1 поля, а не всей структуры.
Ссылка точно актуальна не для всех, но (не)нормальность компиляторов тут не причём — компиляторы C будут реализовывать используемый в целевой системе C ABI, если только не смогут доказать, что компилируемая функция никогда не будет вызвана извне (включая из другой единицы компиляции того же проекта).
Вах, какая прелесть для обфускации :)
Не понял, откуда взялась ".z" в параграфе "Выделенный инициализатор"?
Вот что-то вообще не понимаю как такая магия происходит:
void (*fp)(void) = f;
void (*fp)(void) = *f;
void (*fp)(void) = &f;
void (*fp)(void) = ******f;
void (*fp)(void) = &***********f;
void (*fp)(void) = ***&***f;
void (*fp)(void) = &**&***&***&f;
В Си имя функции может использоваться как указатель на эту же функцию, по сути имя само представляет собой адрес.
Например, функция qsort() из стандартной библиотеки имеет такой прототип:
void qsort(void *base, size_t nmemb, size_t size,
int (*compar)(const void *elem1, const void *elem2));
Последний аргумент (compar) это указатель на функцию, которая будет сравнивать два элемента (elem1 и elem2) массива и возвращать значение типа int.
Допустим, есть массив строк и мы решили их отсортировать. А сравнивать будем стандартной функцией strcmp(). Она имеет подобный профиль с compar, т.е. возвращает значение типа int и принимает два указателя.
Вызов qsort() будет таким (на первые три аргумента не смотрим - написано от балды):
/* берем адрес функции strcmp */
qsort(strings, 42, 42, &strcmp);
/* или можем опустить &, ничего от этого не поменяется */
qsort(strings, 42, 42, strcmp);
В примере, приведенном в статье, это свойство имени функции и используется, т.к. разыменование этого имени-указателя результатом все равно будет этот же адрес функции. И при взятии адреса через имя результатом будет тот же адрес функции. Это можно повторять почти сколько угодно раз - результат будет тот же - адрес этой функции.
многосимвольные константы
Перечисление по умолчанию является int, длина которого в байтах зависит от архитектуры, так что многосимвольные константы больше 4 символов использовать совсем не стоит. А лучше вообще не использовать.
sizeof
Ключевое слово, разворачивающееся на этапе компиляции. Так что если сделать что-то хитровывернутое на макросах на этапе компиляции (хэш посчитать, например), то с -О0 в константу оно не развернется.
макросы
Да, это полноценный язык типа РЕФАЛа и с их помощью много чего можно сделать. Но лучше просто не надо. Мне как-то раз пришлось искать автора чего-то красивого на макросах, чтобы стрясти документацию и пиво за потраченные нервы. Почти "Джей и Молчаливый Боб", да.
Удивительно, какой запас гибкости заложен в язык, крайне избыточный. И сколько же боли принесло это создателям парсеров кода.
Есть лучше пример использования указателей на массивы.
Пусть нужно передать в функцию массив длиной, известной на этапе компиляции (пусть это 3). Тогда вместо известного
void foo(double Arr[], int Size);
можно написать
void foo(double (* Arr)[3]);
На практике это конечно скорее всего будет оформлено покрасивее
typedef double t_vec3[3];
void foo(t_vec3 * Vec);
foo(4, 5); // a=4, b=5, c=3, d=5
Разве d != 4?
Некоторые малоизвестные фичи, фокусы и причуды языка C