Как стать автором
Обновить

Комментарии 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? Сомнительно что-то

С чего это?

C99 Annex J.2

Первая часть не понятна. 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.

  1. Мы можем прочитать мусор, позже это опознаем и выкинем.

  2. Мы можем зациклиться если это "преследование указателя". Вариант теоретический, но надо иногда проверять а не читаем ли мы мусор до того как указатель догоним.

  3. Мы можем упасть в сегфолт. Самое неприятное. Можно проверить, действительно ли эту память выделяли. Дорого, ситуация редкая а проверять придется постоянно. Можем перехватить сегфолт, проверить а не в определенной ли точке кода он случился, и если там то 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. Но идея та же.

Похоже фразой про нюансы я случайно задел чьи то чувства. Может все таки поясните свою позицию? Какое из следующих утверждений является неверным?

  1. макроассемблер ЯВУ Си не имеет встроенных средств отличия "распределенной" памяти от "нераспределенной".

  2. Он полагается в различении этих категорий в масштабах страниц на железо и ОС

  3. В масштабах байт статус "распределенности" не играет никакой роли, важно "назначение". Использование памяти "не по назначению" приведет к UB, даже если она перед этим была "распределена" маллоком, на стеке или еще как-то. За "назначение" отвечают компилятор, линковщик, загрузчик процесса, процессор - за свои конечные подмножества случаев, программист - за все остальное.

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

  5. Из пунктов 2-4 следует что чтение данных по произвольному адресу не является UB пока ОС и железо не против, чего можно добиться способами, к языку не относящимися.

Не претендую на истину в последней инстанции, но хотелось бы серьезных возражений.

НЛО прилетело и опубликовало эту надпись здесь

А я подумал о том, что они только только убрали из языка всякие 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);

В примере, приведенном в статье, это свойство имени функции и используется, т.к. разыменование этого имени-указателя результатом все равно будет этот же адрес функции. И при взятии адреса через имя результатом будет тот же адрес функции. Это можно повторять почти сколько угодно раз - результат будет тот же - адрес этой функции.

  1. многосимвольные константы

    Перечисление по умолчанию является int, длина которого в байтах зависит от архитектуры, так что многосимвольные константы больше 4 символов использовать совсем не стоит. А лучше вообще не использовать.

  2. sizeof

    Ключевое слово, разворачивающееся на этапе компиляции. Так что если сделать что-то хитровывернутое на макросах на этапе компиляции (хэш посчитать, например), то с -О0 в константу оно не развернется.

  3. макросы

    Да, это полноценный язык типа РЕФАЛа и с их помощью много чего можно сделать. Но лучше просто не надо. Мне как-то раз пришлось искать автора чего-то красивого на макросах, чтобы стрясти документацию и пиво за потраченные нервы. Почти "Джей и Молчаливый Боб", да.

Удивительно, какой запас гибкости заложен в язык, крайне избыточный. И сколько же боли принесло это создателям парсеров кода.

Есть лучше пример использования указателей на массивы.
Пусть нужно передать в функцию массив длиной, известной на этапе компиляции (пусть это 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?

Зарегистрируйтесь на Хабре, чтобы оставить комментарий

Публикации