В этом посте разобраны некоторые фокусы, причуды и фичи языка C (некоторые из них – весьма фундаментальные!), которые, казалось бы, могут сбить с толку даже опытного разработчика. Поэтому я потрудился сделать за вас грязную работу и (в произвольном порядке) собрал некоторые из них в этом посте. Примеры сопровождаются ещё более вольными краткими пояснениями и/или листингами (некоторые из них цитируются).

Конечно же, здесь я не берусь перечислять абсолютно всё, так как факты из разряда «функция nan() не может устанавливать errno, поскольку в определённых ситуациях поведёт себя как strtod()» не слишком интересны.

ВНИМАНИЕ: сам факт попадания тех или иных вещей в эту подборку  не означает автоматически, что я рекомендую или, наоборот, не рекомендую ими пользоваться! Некоторые из приведённых примеров никогда не должны просачиваться за пределы списков наподобие этого, тогда как другие примеры невероятно полезны! Уверен, что могу положиться на ваш здравый смысл, дорогие читатели.

Источники

Скрытый текст

Указатели на массив

При спуске к указателю обычные указатели на массив, как правило, оказываются не нужны:

int arr[10];

int *ap0 = arr;        // спуск к указателю
// ap0[2] = ...

int (*ap1)[10] = &arr; // правильный указатель на массив
// (*ap1)[2] = ...

Но как приятно, когда можешь выделить в куче большой многомерный массив:

int (*ap3)[90000][90000] = malloc(sizeof *ap3);

С указателями могут пригодиться даже массивы переменной длины

int (*ap4)[n] = malloc(sizeof *ap4);

Оператор запятая

Запятая применяется в качестве оператора и ставится как разделительный знак между двумя и более выражениями в случаях, когда ожидается всего одно выражение. Когда для того, чтобы получить значение, требуется интерпретировать целый набор выражений, то учитывается только крайнее правое.

Например: b = (a=3, a+2); – этот код сначала присвоит выражение 3 переменной a, затем a+2 будет присвоено переменной b. Таким образом, в итоге b будет содержать значение 5, тогда как в переменной a будет записано 3.

В Википедии приводится ещё несколько таких примеров.

Диграфы, триграфы и альтернативные токены

Код на C не всегда поддаётся портированию, но сам язык C, пожалуй, приспособлен к портированию лучше любого другого. Например, есть системы, в которых вместо ASCII используется другая кодировка, скажем, EBCDIC. Для поддержки таких систем в C предусмотрены диграфы и триграфы – многосимвольные последовательности, трактуемые компилятором как другие символы.

Диграф

 

 

Триграф

 

 

iso646.h

 

<:

[

 

??=

#

 

and

&&

:>

]

 

??(

[

 

and_eq

&=

<%

{

 

??/

\

 

bitand

&

%>

}

 

??)

]

 

bitor

|

%:

#

 

??'

^

 

compl

~

%:%:

##

 

??<

{

 

not

!

——–

———–

 

??!

|

 

not_eq

!=

——–

———–

 

??>

}

 

or

||

——–

———–

 

??-

~

 

or_eq

|=

——–

———–

 

——–

———–

 

xor

^

——–

———–

 

——–

———–

 

xor_eq

^=

Хотя и пришлось преодолеть вялое сопротивление, Комитет постановил убрать поддержку триграфов, начиная с версии C23.

Скрытый текст

Выделенный инициализатор

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

struct Foo {
    int x, y;
    const char *bar;
};

void f(void)
{
    int arr[] = { 1, 2, [5] = 9, [9] = 5, [8] = 8 };

    struct Foo f = { .y = 23, .bar = "barman", .x = -38 };

    struct Foo arr[] = {
        [10] = {      8,  8,      9 },
         [8] = {      1,  8,   bar3 },
        [12] = { .x = 9,     .z = 8 },
    };

    struct {
        int sec, min, hour, day, mon, year;
    } z = { 
        .day = 31, 12, 2014, 
        .sec = 30, 15, 17
    }; // инициализирует z в { 30, 15, 17,  31, 12, 2014 }
}
Скрытый текст

Составные литералы

Составной литерал выглядит как приведение списка инициализаторов, заключённого в скобки. Его значение — это объект того типа, что указан при приведении, и в нём содержатся элементы, указанные в инициализаторе.

#include <stdio.h>

struct Foo { int x, y; };

void bar(struct Foo p)
{
    printf("%d, %d", p.x, p.y);
}

int main(void)
{
    bar((struct Foo){2, 3});
    return 0;
}
Скрытый текст

Составные литералы — это адреса

(struct Foo){};
((struct Foo){}).x = 4;
&(struct Foo){};
func(&(struct Foo){.x = 2});

Даже если вы уже знаете о составных литералах, возможно, вы даже не замечали, что они представляют собой адреса (lvalues). А это важно, поскольку, когда перед нами lvalue, мы можем получить его адрес (и, например, передать его функции).

Защита от затенения

Следующий код вернёт 42, а не 3840!

int x = 42;

int func() {
    int x = 3840;
    {
        extern int x;
        return x;
    }
}

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

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

enum state {
    waiting = 'WAIT',
    running = 'RUN!',
    stopped = 'STOP',
};

Например, у меня на машине я могу локализовать 'WAIT' как показано здесь:

00001120: c3 66 66 2e 0f 1f 84 00 00 00 00 00 0f 1f 40 00  .ff...........@.
00001130: f3 0f 1e fa e9 67 ff ff ff 55 48 89 e5 48 83 ec  .....g...UH..H..
00001140: 10 c7 45 fc 54 49 41 57 8b 45 fc 89 c6 48 8d 05  ..E.TIAW.E...H..
00001150: b0 0e 00 00 48 89 c7 b8 00 00 00 00 e8 cf fe ff  ....H...........
00001160: ff b8 00 00 00 00 c9 c3 f3 0f 1e fa 48 83 ec 08  ............H...

Битовые поля

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

struct cat {
    unsigned int legs  : 3;  // 3 бита на лапы  (0-4 умещается в 3 бита)
    unsigned int lives : 4;  // 4 бита на жизни (0-9 умещается в 4 бита)
};
Скрытый текст

Битовые поля нулевой длины

Скрытый текст

Описание из документации к Arm Compiler 6:

При помощи битового поля нулевой длины можно вносить следующие изменения:

  • Разграничивать любые битовые поля, расположенные до и после битового поля нулевой длины. Любые битовые поля по разные стороны от этой границы трактуются как неперекрывающиеся участки памяти. Это сказывается на структуре программ C и C++. В стандартах C и C++ требуется, чтобы обращения к битовому полю на загрузку и на сохранение с одной стороны границы никак не затрагивали битовые поля по другую сторону границы.

  • Пользуйтесь заполняющими нулями, чтобы все битовые поля после битового поля нулевой длины выравнивались по ближайшей доступной естественной границе в зависимости от того, каков тип битового поля нулевой длины. Например, при помощи char:0 можно выполнить выравнивание по ближайшей доступной байтовой границе, а при помощи int:0 – по ближайшей доступной границе слов.

Рассмотрим пример, приведённый в качестве ответа на Stackoverflow (с небольшими изменениями):

struct bar {
    unsigned char x : 5;
    unsigned short  : 0;
    unsigned char y : 7;
}

Вот как будет выглядеть в памяти вышеприведённый код (предполагается, что мы имеем 16-разрядные short и не учитываем порядок битов – от младшего к старшему или наоборот):

char pad pad      short boundary
 |    |   |        |
 v    v   v        v
 xxxxx000 00000000 yyyyyyy0

Битовое поле нулевой длины расположено так, что позиция сдвигается до следующей границы short (иными словами: расположится на ближайшем естественном рубеже, предусмотренном на целевой платформе). Мы определили short как 16-разрядное число. Соответственно, 16 минус 5 равно 11, значит, 11 разрядов нужно заполнить нулями.

Квалификатор типа volatile 

Этот квалификатор сообщает компилятору, что к переменной можно обратиться не только через данный код, но и иначе (например, если мы имеем дело с устройством для ввода/вывода через память). Поэтому нельзя допускать, чтобы в процессе оптимизации убирались операции чтения и записи, связанные с этим ресурсом.

Скрытый текст

Квалификатор типа restrict 

Добавляя такой квалификатор типа, программист сообщает компилятору, что на время жизни данного указателя ни один другой указатель не предназначен для обращения к объекту, на который направлен рассматриваемый указатель. Таким образом, компилятор может выполнять оптимизации (например, векторизовать код), которые в ином случае были бы невозможны.

Скрытый текст

Квалификатор типа register 

Подсказывает, что компилятор хранит объявленную переменную в регистре ЦП (или в каком-то другом месте, где к ней можно быстро обратиться), а не в памяти с произвольным доступом. Если переменная объявлена с таким квалификатором, то к её местоположению обратиться нельзя (правда, можно применить оператор sizeof).

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

Элемент массива с динамическим размером

Из Википедии:

struct vectord {
    short len;    // здесь должен быть, как минимум, ещё один член данных
    double arr[]; // элемент массива с динамическим размером должен идти последним

    // Здесь компилятор может зарезервировать дополнительное место для заполнения нулями,
    //   как и при работе с членами структур.
};

struct vectord *vector = malloc(...);
vector->len = ...;
for (int i = 0; i < vector->len; ++i) {
     vector->arr[i] = ...;  // прозрачно использует требуемый тип (число double)
}
Скрытый текст

Описатель формата  %n 

В этом ответе на StackOverflow о нём рассказано достаточно хорошо:

%n возвращает ту позицию, в которой сейчас находится воображаемый курсор, когда форматируется вывод printf().

int pos1, pos2;
const char *str_of_unknown_len = "we don't care about the length of this";

printf("Write text of unknown %n(%s)%n length\n", &pos1, str_of_unknown_len, &pos2);
printf("%*s\\%*s/\n", pos1, " ", pos2-pos1-2, " ");
printf("%*s", pos1+1, " ");
for (int i = pos1+1; i < pos2-1; ++i) {
    putc('-', stdout);
}
putc('\n', stdout);

Вывод будет таким:

Write text of unknown (we don't care about the length of this) length
                      \                                      /
                       -------------------------------------

Конечно, этот пример немного надуманный, но сами приёмы могут пригодиться, когда нужно аккуратно вывести текст.

Описатель формата  %.* (минимальная ширина поля)

В случаях, когда хотите заполнять память переменным количеством символов, вместо

char fmt_buf[MAX_BUF];
snprintf(fmt_buf, MAX_BUF, "%%.%df", prec);
printf(fmt_buf, num);

попробуйте

printf("%.*f", prec, num);

Другие относительно малоизвестные описатели форматов

Загляните в §7.21.6.1 и §7.21.6.2 в черновом стандарте C11. Там вы увидите %#, %e, %-, %+, %j, %g, %a и ещё несколько интересных описателей.

Пересекающиеся синтаксические конструкции

Следующий код на C является синтаксически корректным:

#include <stdio.h>

int main()
{
    int n = 3;
    int i = 0;

    switch (n % 2) {
        case 0:
            do {
                ++i;
        case 1:
                ++i;
            } while (--n > 0);

    }

    printf("%d\n", i); // 5
}

Знаю, что программисты, опасающиеся goto, могли бы написать так:

    switch (x) {
        case 1:
            // 1 специфичный код

      if (0) {
        case 2:
            // 2 специфичный код
      }

            // общее для 1 и 2
    }

Самый известный пример использования этой причуды/«фичи» — это метод Даффа:

send(to, from, count)
    register short *to, *from;
    register count;
{
    register n = (count + 7) / 8;
    switch (count % 8) {
    case 0: do { *to = *from++;
    case 7:      *to = *from++;
    case 6:      *to = *from++;
    case 5:      *to = *from++;
    case 4:      *to = *from++;
    case 3:      *to = *from++;
    case 2:      *to = *from++;
    case 1:      *to = *from++;
            } while (--n > 0);
    }
}

"оператор" --> 

Следующий код на C корректен:

size_t n = 10;
while (n --> 0) {
    printf("%d\n", n);
}

Уместен вопрос: с каких это пор в C есть такой оператор? И я отвечу: ни с каких. --> - это не один, а два разных оператора,  -- и >, записываемых друг за другом. Поэтому они выглядят как один. Это допустимо, поскольку C снисходительно относится к пробелам.

n --> 0 эквивалентно (n--) > 0

idx[arr] 

Нотация с квадратными скобками для обращения к элементам массива – это синтаксический сахар, применяемый в арифметике указателей:

arr[5] ≡ *(arr + 5) ≡ *(5 + arr) ≡ 5[arr]

Категорически недопустимо использовать такое в реальном коде … а вообще выглядит довольно забавно!

// массив[индекс]
boxes[products[myorder.product].box].weight;

// индекс[массив]
myorder.product[products].box[boxes].weight;

Отрицательные индексы массива

Однажды, когда мне требовалось по-быстрому провести отладку, я решил проверить, корректными ли значениями заполнено пространство в конце массива. Но я не знал, откуда именно начинается заполнение. Поэтому я сделал так:

int *end = arr + (len - 1);
if (end[0] == VAL && end[-1] == VAL && end[-5] == VAL) {
    puts("Correct padding");
}

Конкатенация строковых литералов

Вам не требуется ни sprintf() (ни strcat()!) для конкатенации строковых литералов:

#define WORLD "World!"
const char *s = "Hello " WORLD "\n"
                "It's a lovely day, "
                "innit?";

Сращивание строк при помощи обратного слэша

Удаляются все экземпляры обратного слэша \, за которыми непосредственно следует символ перехода на новую строку. Таким образом, физические строки исходного кода сращиваются, образуя логические.

#define I_AM_O\
NE_MACRO 123

// Я комментарий \
   Я всё тот же комментарий. \
   Я – так называемый комментарий-ОДНОСТРОЧНИК!


int fun()
{
    if (drive == 2) // дисковод 2 это C:\
        return 1;  <-- мой дружок вот тут тоже входит в состав КОММЕНТАРИЯ!!

    writestuff();
    return 0;
}

int main()
{
    int x = I_AM_ONE_MACRO;  // корректно расширяется до 123

    int same_\
variable = 1;
    same_variable = 1;

    const char *p = "String with\
                     so many spaces in the MIDDLE!";

    puts(p); // Строка с таким множеством                   пробелов в середине!"

    return 0;
}

Использование && и || в качестве условных операторов 

Если вы пишете шелл-скрипты, то понимаете, о чём я.

#include <ctype.h>
#include <stdio.h>
#include <stdbool.h>

int main(void)
{
    1 && puts("Hello");
    0 && puts("I won't");
    1 && puts("World!");
    0 && puts("be printed");
    1 || puts("I won't be printed either");
    0 || puts("But I will!");

    true && (9 > 2) && puts("9 is bigger than 2");

    isdigit('9') && puts("9 is a digit");
    isdigit('n') && puts("n is a digit") || puts("n is NOT a digit!");

    return 0;
}

Вероятно, компилятор будет сильно ругаться, так как в коде C такая практика очень нетипична.

Проверка допущений при компиляции при помощи перечислений

#define D 1
#define DD 2

enum CompileTimeCheck
{
    MAKE_SURE_DD_IS_TWICE_D = 1/(2*(D) == (DD)),
    MAKE_SURE_DD_IS_POW2    = 1/((((DD) - 1) & (DD)) == 0)
};

Может пригодиться при работе с библиотеками, константы в которых можно конфигурировать во время выполнения.

Ситуативное определение struct в возвращаемом типе функции

Можно определять структуры (struct) в самых (на первый взгляд) произвольных местах:

#include <stdio.h>

struct Foo { int a, b, c; } make_foo(void) {
    struct Foo ret = { .c = 3 };
    ret.a = 11 + ret.c;
    ret.b = ret.a * 3;
    return ret;
}

int main()
{
    struct Foo x = make_foo();
    printf("%d\n", x.a + x.b + x.c);
    return 0;
}

«Вложенное» определение struct необязательно держать вложеннным

#include <stdio.h>

struct Foo {
    int x;
    struct Bar {
        int y;
    };
};

int main()
{
    struct Bar s = { 34 };  // правильно
    // struct Foo.Bar s;    // неправильно
    printf("%d\n", s.y);
    return 0;
}

Плоские списки инициализации

int arr[3][3] = { 1, 2, 3, 4, 5, 6, 7, 8, 9 };
//            = { {1,2,3}, {4,5,6}, {7,8,9} };


struct Foo {
    const char *name;
    int age;
};

struct Foo records[] = {
    "John",   20,
    "Bertha", 40,
    "Andrew", 30,
};

Неявное приведение указателей void

C11 §6.3.2.3 ¶1:

Указатель на void можно преобразовывать в указатель на объект любого типа и обратно. Указатель на объект любого типа можно преобразовывать в указатель на void и обратно. При сравнении результат получится равным исходному указателю.

C11 §6.5.16.1 ¶1:

У левого операнда атомарный, квалифицированный или неквалифицированный указатель и (с учётом того типа, который будет у левого операнда после преобразования lvalue) один из операндов указывает на объектный тип, а другой – на квалифицированную или неквалифицированную версию void. Тот тип, на который указывается слева, обладает всеми теми же квалификаторами, что и тип, на который указывается справа;

void* был добавлен в C89, поскольку требовался обобщённый тип указателя, который поддаётся неявному преобразованию в обе стороны.

На самом деле, при явном преобразовании указателей void возникают следующие проблемы:

  • Это просто не нужно, так как void* автоматически и безопасно расширяется до указателя любого другого типа;

  • В таком случае код замусоривается, приведения не очень удобно читать (особенно если указатель относится к типу long);

  • Такой подход приводит к самоповторам;

  • Могут возникать скрытые ошибки, если возвращаемый тип изменится с void* на что-либо более конкретное.

Статические индексы массивов в объявлениях параметров функций

За исключением некоторых контекстов, если имя массива не сопровождается индексом (например, region вместо region[4]), то перед нами указатель, значением которого является адрес первого элемента в массиве – при условии, что ранее этот массив был объявлен. Тип массива в списке параметров функции также преобразуется в тип соответствующего указателя. Информация о том, какого размера был массив аргументов, теряется, если обратиться к массиву извне тела функции.

Чтобы сохранять эту информацию, которая может пригодиться при оптимизации, в C99 разрешено объявлять индекс массива аргументов при помощи ключевого слова static. В константном выражении указывается минимальный размер указателя, и на эту информацию можно опираться при оптимизациях как на допущение. Крайне рекомендуется именно так использовать ключевое слово static. Это ключевое слово может фигурировать только в самой внешней операции приведения типа массива и только в объявлениях параметров функций. Если та сторона, которая вызывает функцию, не соблюдает этих ограничений, то возникает неопределённое поведение.

В следующих примерах показано, как может использоваться данная возможность:

int n;
void foo(int arr[static 10]);       // arr указывает на первое целое число, а всего таких чисел не менее 10 
void foo(int arr[const 10]);        // arr - это константный указатель
void foo(int arr[const]);           // константный указатель на целое число
void foo(int arr[static const n]);  // arr указывает на не менее чем n целых чисел (массив переменной длины)

void foo(int p[static1]); — это, фактически, стандартный вариант объявления, что p должен указывать не на null.

Перегрузка макросов путём регулирования длины списка аргументов

Скрытый текст
#include <stdio.h>
#include "cmoball.h"

#define NoA(...) CMOBALL(FOO, __VA_ARGS__)
#define FOO_3(x,y,z) "Three"
#define FOO_2(x,y)   "Two"
#define FOO_1(x)     "One"
#define FOO_0()      "Zero"


int main()
{
    puts(NoA());
    puts(NoA(1));
    puts(NoA(1,1));
    puts(NoA(1,1,1));
    return 0;
}

При работе с typedef применяется такой же синтаксис, как и с любыми другими спецификаторами 

Обычно при объявлении новых типов применяется следующий синтаксис:

typedef unsigned char byte;

typedef struct {
    int x;
    int y;
    const char *p;
} Record;

Но ключевое слово typedef может располагаться и иначе:

unsigned typedef char byte;

struct {
    int x;
    int y;
    const char *p;
} typedef Record;

Причём у нас всё равно сохраняется возможность объявить множество типов за один ход:

struct {
    int x;
    int y;
    const char *p;
} typedef Record, record, *record_ptr;

Типы функций

Указатели функций широко известны, но не менее известно, что их синтаксис немного неуклюжий. С другой стороны, не столь известно, что typedef можно создавать не только для большинства типов объектов, но и для типов функций.

#include <stdio.h>

int main()
{
    typedef double fun_t(double);
    fun_t sin, cos, sqrt;
    fun_t *ftpt = &sqrt;

    printf("%lf\n", ftpt(4)); // 2.000000

    return 0;
}

Странности во взаимосвязях между обозначениями функций и указателями

Пример от u/AnonymouX47, приведённый на Reddit в посте What your weirdest C feature?:

Допустим, у нас есть простой прототип функции: void f(void);

Следующие строки эквивалентны друг другу:

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;

Следующие строки также эквивалентны друг другу:

f();
(*f)();
(&f)();
(*&f)();
fp();
(*fp)();
(*&fp)();
(****fp)();
(&******fp)();
(**&**fp)();
(*&*&*&*fp)();

Но (&fp)() или (&*&*&fp)() работать не будет.

X-макросы

Скрытый текст

Именованные параметры функций

struct _foo_args {
    int num;
    const char *text;
};

#define foo(...) _foo((struct _foo_args){ __VA_ARGS__ })
int _foo(struct _foo_args args)
{
    puts(args.text);
    return args.num * 2;
}

int main(void)
{
    int result = foo(.text = "Hello!", .num = 8);
    return 0;
}

Сочетание аргументов по умолчанию, именованных и позиционных аргументов

Используем составные литералы и макросы для создания именованных аргументов:

typedef struct { int a,b,c,d; } FooParam;
#define foo(...) foo((FooParam){ __VA_ARGS__ })
void (foo)(FooParam p);

Добавить аргументы по умолчанию также не составляет труда:

#define foo(...) foo((FooParam){ .a=1, .b=2, .c=3, .d=4, __VA_ARGS__})

Но после этого позиционные аргументы перестанут действовать; кроме того, могут возникать ситуации, в которых вам захочется поддерживать оба варианта. Но недавно я обнаружил, что можно заставить такие комбинации работать, добавив формальный параметр:

typedef struct { int _; int a,b,c,d; } FooParam;
#define foo(...) foo((FooParam){ .a=1, .b=2, .c=3, .d=4, ._=0, __VA_ARGS__})

Теперь foo можно вызывать следующими способами:

foo();           // a=1, b=2, c=3, d=4
foo(.a=4, .b=5); // a=4, b=5, c=3, d=5
foo(4, 5);       // a=4, b=5, c=3, d=5
foo(4, 5, .d=8); // a=4, b=5, c=3, d=8

Формальный параметр не требуется, если у вас есть аргументы, которые требуется передавать по имени:

typedef struct { int alwaysNamed; int a,b,c,d; } FooParam;
#define foo(...) foo((FooParam){.a=1,.b=2,.c=3,.d=4, .alwaysNamed=5, __VA_ARGS__})

Злоупотребляем объединениями, чтобы группировать сущности по пространствам имён  

Допустим, у вас есть структура struct с набором полей, и некоторые из них вы хотите обрабатывать одновременно, как группу под некоторым именем. Возможно, вы хотите, чтобы можно было с удобством копировать их единым блоком, применяя при этом операцию присваивания struct.

Работая с объединениями, можно обращаться одновременно к a.field2 и a.sub (причём, a.field2 равноценно a.sub.field2) без каких-либо макросов.

struct a {
    int field1;
    union {
        struct {
            int field2;
            int field3;
        };
        struct {
            int field2;
            int field3;
        } sub;
    };
};

Единичные сборки

Поскольку механизм #include сводится к примитивному копированию и вставке содержимого включённого файла в актуальный код, в C допускается создание так называемых единичных сборок, где мы складываем весь код в одну единицу трансляции.

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

Также будет сложно генерировать compile_commands.json для инструментов, работающих на базе LLVM.

Сопоставление классов символов при помощи sscanf() 

Из этого комментария на Reddit:

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

int len = 0;
char buf[256];
int read_token = sscanf(input, "%255[a-zA-Z_]", buf, &len);
if (read_token) { /* что-то делаем */ }

или пропустить символы пробелов:

int len = 0;
char buf[256];
sscanf(input, "%255[\r\n]%n", buf, &len);
input += len;

Сборщик мусора

Boehm GC – это библиотека, обеспечивающая сборку мусора в C и C++

Cosmopolitan Libc

Описание с сайта проекта:

Благодаря Cosmopolitan Libc, язык C переходит в состояние «собрал один раз — запустил везде», подобно Java. Но при этом не требуется не интерпретатора, ни виртуальной машины. Вместо этого инструмент реконфигурирует имеющиеся GCC и Clang, давая на вывод POSIX-совместимый многоязычный формат, который нативно выполняется на Linux + Mac + Windows + FreeBSD + OpenBSD + NetBSD + BIOS при максимально возможной производительности. При этом сборка занимает в памяти такой минимум места, какой только можно представить.

Ассемблерные вставки

Язык C, будучи высокоуровневым, достаточно хорошо коммуницирует с низкоуровневым миром. Можно создать код на ассемблере и без труда связать его так, чтобы он работал в программе, написанной на C. Кроме того, во многих компиляторах в качестве расширения (указано в приложении J к стандарту C) предлагается такая возможность, как ассемблерная вставка, обычно предваряемая в коде ключевым словом asm.

Скрытый текст

Вычисление sizeof во время компиляции с возникновением сопутствующей ошибки duplicate case

Допустим, вы разрабатываете встраиваемую систему или вообще любой проект, где получить вывод printf() может быть не так просто.

int foo(int c)
{
    switch (c) {
        case sizeof (struct Foo): return c + 1;
        case sizeof (struct Foo): return c + 2;
    }
}

Если добавить такую простую функцию где-либо в вашем коде, то (в зависимости от компилятора) можно получить сообщение об ошибке, в котором содержится результат операции sizeof.

error: duplicate case value '16'
        case sizeof(struct Foo): return c + 2;

Обнаружение константных выражений

#define ICE_P(x) _Generic((1 ? ((void*)((x)*(uintptr_t)0)) : &(int){1}), int*: 1, void*: 0)

Отсюда: I think I found a C11 compliant way to detect constant expressions : r/C_Programming

TL;DR: Вызов ICE_P результирует в true, если аргументом служит константное выражение, а в противном случае результирует в false. Поэтому следующий код:

int x = 3;
printf("%d %d\n", ICE_P(x), ICE_P(3));

должен дать вывод 0 1.

Объектно-ориентированное программирование

Скрытый текст

Безопасные функции с переменным количеством аргументов

Фокус от NRK:

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

Существует и такая проблема: подобная функция не знает, сколько элементов будет ей передано, поэтому тот, кто вызывает функцию, должен уточнять это вручную – либо при помощи некого сторожевого значения, обозначающего конец списка аргументов (напр., в execl() в таком качестве используется NULL), либо передавая размер в другом параметре.

Если грамотно пользоваться макросами для работы с такими функциями — эти макросы появились в C99 — сочетая их с составными литералами, то все эти недостатки можно легко обойти.

Нижеприведённый пример работает лишь в том случае, если все параметры относятся к одному и тому же типу, а бывает так, что могут потребоваться разные типы. Но должна быть предусмотрена возможность расширять его, применяя «размеченные объединения» и _Generic, предусмотренный в C11.

#include <stdio.h>

void print_vec(FILE *f, const int *v, size_t n)
{
    for (size_t i = 0; i < n; ++i) {
        fprintf(f, "%d\n", p[i]);
    }
}
#define print_vec(fstream, ...)                                     \
    print_vec((fstream),                                            \
              (const int[]){ __VA_ARGS__ },                         \
              (sizeof (int[]){ __VA_ARGS__ } / sizeof (int)) )

int main(void)
{
    print_vec(stdout, 1);
    print_vec(stdout, 1, 2, 3);
    print_vec(stdout, 1, 2, 3, 4, 5);
    return 0;
}

Метапрограммирование

В C11 добавилась возможность _Generic, но оказывается, что метапрограммирование возможно даже на чистом C99 (правда, для этого требуется бесчеловечно надругаться над препроцессором). Знакомьтесь с библиотекой Metalang99.

#include <datatype99.h>

datatype(
    BinaryTree,
    (Leaf, int),
    (Node, BinaryTree *, int, BinaryTree *)
);

int sum(const BinaryTree *tree) {
    match(*tree) {
        of(Leaf, x) return *x;
        of(Node, lhs, x, rhs) return sum(*lhs) + *x + sum(*rhs);
    }

    return -1;
}

Препроцессор — это полноценный язык

Я уже упоминал некоторые фокусы с препроцессором, но на самом деле их гораздо больше! На самом деле, я мог бы написать ещё такую же статью об одних только фокусах препроцессора. В конце концов, это самодостаточный Тьюринг-полный язык с собственными правилами, грамматикой и подводными камнями. Да что там, он даже применяется не только с C – есть горячие головы,  сочетающие его, например, с JavaScript

К счастью, Тима Кинсарт, разработчик вышеупомянутой библиотеки Metalang99, уже собрал список awesome-c-preprocessor, где описаны всевозможные разумные и неразумные вещи, которые можно проделать с препроцессором C.

Комментарии (43)


  1. RodionGork
    03.10.2024 13:55
    +5

    Неплохая затея, но быть может стоит подобные списки разбивать на "нужные" и "забавные" - и чуть ли не отдельными постами предлагать :) Всё-таки он уж очень длинным получился.


    1. rukhi7
      03.10.2024 13:55

      так написано же: фичи, фокусы и причуды языка C

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


  1. Yak52
    03.10.2024 13:55
    +6

    Сращивания строк или о вреде комментариев.

    Как то еще очень давно был у меня на С вот такой кусок кода:

    if(disk == 0)
    	Что то делаем только для корневого каталога диска С
    Что то делаем всегда
    	

    И вот решил я добавить комментарий

    if(disk == 0)  // Обращаемся к C:\
    	Что то делаем только для корневого каталога диска С
    Что то делаем всегда
    	

    Долго не мог понять почему по условию вместо выполнения кода в строке 2, выполняется код в строке 3. И да, раскраску кода в редакторе, тогда еще не завезли.


    1. Mingun
      03.10.2024 13:55
      +14

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


      1. naviUivan
        03.10.2024 13:55
        +1

        Тут больше о проблеме использования "магических" констант и необходимости коментария там, где этого можно избежать заменив "0" на дефайн или енум с описательным именем.


      1. VoodooCat
        03.10.2024 13:55

        Код в котором форсированы скобки, обычно не страдает. Однако код в котором скобки не обязательны для простых случаев - легче читается и более лаконичен. То что перенос строки экранируется, даже для комментариев - ну это надо знать. Так или иначе - такая конструкция будет использоваться обоснованно и надо знать как она работает. Иначе говоря, скобки ничего не порешают. И в целом такие ошибки легко определяются. Вместо предложенных скобок можно предложить линтер/анализ на потенциальные узкие места.


      1. ZyXI
        03.10.2024 13:55
        +2

        Не факт. Если вы пишете со скобками так:

        if (1)  // \
        {
            code();
        }
        

        то да, но вот так:

        if (1) {  // \
            code();
        }
        

        компилятор вполне проглотит if (1) {}. Следующее выражение в этом случае не станет условным, как в случае совсем без скобок, но вот, например, echo 'int main(void) { if (1) {} }' | clang -std=c99 -pedantic -Wall -Weverything -xc - даже не производит никаких предупреждений.


    1. Zara6502
      03.10.2024 13:55
      +1

      это лишь говорит о каких--то условностях устоявшихся для языка и, на мой взгляд, застрявших в прошлом. Берем C# и пишем

      int a = 1;
      if (a == 1) // комментарий
          Console.WriteLine(a);
      Console.WriteLine(--a);

      на выходе получаем

      1

      0

      что логично.


      1. a-tk
        03.10.2024 13:55
        +13

        Вы видимо не поняли в чём прикол.

        Символ \ в конце строки означает продолжение выражения на следующей строке. В том числе комментария. Таким образом, комментарий "обращаемся к C:\" приводит к тому, что следующая строка также является комментарием, а не кодом.


        1. Zara6502
          03.10.2024 13:55
          +1

          Спасибо за разъяснения. А кому-то легче минус вляпать чем написать что почём.


  1. Alex-111
    03.10.2024 13:55
    +1

    Вычисление 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'


  1. event1
    03.10.2024 13:55

    Диграфы, триграфы и альтернативные токены

    Мы всё дальше и дальше от бога...

    Битовые поля ... Вот как будет выглядеть в памяти вышеприведённый код

    Упаковка битовых полей в памяти полностью зависит от реализации. Вот из вики, например:

    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


    1. redsh0927
      03.10.2024 13:55
      +4

      Упаковка битовых полей в памяти полностью зависит от реализации.

      Большая часть "implementaion-defined" в классическом Си - подстраховка на случай всяких экзотических архитектур и реализаций... На обычных процессорах всё однозначно и можно забить посмотреть в мануале к компилятору и спокойно юзать.

      А оно не кастанётся к типу индекса?

      a[b] это абсолютный аналог *(a+b). Просто синтаксический сахар. От перестановки мест слагаемых ничего не меняется.

      Лучше не надо. Обращение за границами массива — это UB

      С чего это? Типичный способ например обрезать пробелы в конце строки -

      char *p = buf + strlen(buf);
      while((p > buf) && isspace(p[-1]))
        p--;
      *p = 0;

      Никаких уб


      1. event1
        03.10.2024 13:55
        +2

        a[b] это абсолютный аналог *(a+b). Просто синтаксический сахар. От перестановки мест слагаемых ничего не меняется.

        То есть int + void* приведётся к тому же типу, что и void* + int? Сомнительно что-то

        С чего это?

        C99 Annex J.2


        1. evgnort
          03.10.2024 13:55

          Первая часть не понятна. 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] можно? А если...? Так то проблема серьезная, но формализация метода решения странная и все игнорят (и я тоже, увы).


          1. a-tk
            03.10.2024 13:55

            А разве void имеет известный размер, чтобы участвовать в адресной арифметике?


            1. evgnort
              03.10.2024 13:55

              Это ж не я предложил так сконвертить. Но с интом тоже странно выглядит.

              А вобще gcc считает размер void за 1, но ругается.

              https://gcc.gnu.org/onlinedocs/gcc/Pointer-Arith.html


          1. event1
            03.10.2024 13:55
            +1

            a[b] это *(a+b), не *(b+a). Что тут void*, что int и почему они должны местами поменяться?

            Речь про то, что нет разницы между (a + b) и (b + a) при кастовании типов, если a — это указатель, а b — целое. Я до сегодня думал, что типы кастуются слева направо. И в вышеприведённых примерах у выражения будет типы type(a) и type(b), в первом и втором случаях, соответственно. Оказалось, что там есть сложная система.

            Про сабскрипт

            На хабре же была отличная статья с тестами про чтение за границами массива. Да я и сам, не так давно, переезжал с uclibc на musl и тоже обнаружил там и сям пару сегфолтов.


            1. evgnort
              03.10.2024 13:55
              +2

              Я до сегодня думал, что типы кастуются слева направо

              та же беда.

              На хабре же была отличная статья с тестами про чтение за границами массива

              Статью прочитал но прямой связи не увидел. Если я объявил что-то как char [16 (константа)] и потом полез к 17му байту (константа) то да, разворот цикла поломается. Наверное. По крайней мере приводится случай когда он поломался. Если я объявил что-то как char *, присвоил этому адрес из середины другого куска (важно что не выделял) и начал прыгать по нему как по массиву (так более читаемо, и выше много примеров что так думаю не я один) - то, возможно, компилятор который на этом сломается я не встречу пока сам не напишу, т.к. массовым ему не стать.


      1. a-tk
        03.10.2024 13:55
        +1

        UB - это обращение к памяти по указателю за пределами распределённой памяти. А получение оперирование адресами такой памяти UB не является. Иначе само существование такой сущности, как нулевой указатель, было бы UB.


        1. event1
          03.10.2024 13:55

          А получение оперирование адресами такой памяти UB не является

          Получение не является, разыменование — является


          1. evgnort
            03.10.2024 13:55

            как бы да, но есть нюансы :)
            https://man7.org/linux/man-pages/man2/userfaultfd.2.html

            За что минус то. Два треда. Один начал что-то читать но подвис. Второй тем временем все переписал. Первый очнулся, прочитал до конца, в том числе ссылку, которая уже не ссылка. Если хочется локфри - а его хочется, то можно или как-то руками проверить - а ссылка ли это, или перехватить сигсегв или вот эту штуку использовать. Вполне себе такой штатный сигсегв получается.


            1. evgnort
              03.10.2024 13:55

              Теоретически это UB. Практически для ситуации описанной выше (там не код а данные, которые мы не пишем а читаем), не такое уж оно и undefined.

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

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

              3. Мы можем упасть в сегфолт. Самое неприятное. Можно проверить, действительно ли эту память выделяли. Дорого, ситуация редкая а проверять придется постоянно. Можем перехватить сегфолт, проверить а не в определенной ли точке кода он случился, и если там то longjmp на выход, а если нет то валимся дальше. Ну или в свежем линуксе сделать тоже самое более цивилизованным способом (я кстати все не соберусь попробовать).

              Британскую королеву код славить точно не начнет. В результате получается усточивость к манипуляциям ядра с процессом и реализуемость некоторых локфри алгоритмов в юзерспейс. Обычно такой трюк для подкачки используется и возможные адреса обращений там все таки резервируются, тут извращенное применение но работает.


              1. Gryphon88
                03.10.2024 13:55

                Мне интересно, что скажет на это компилятор с -О3... если это уб, то он может просто выпилить.


                1. evgnort
                  03.10.2024 13:55

                  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;
                  	}


                  1. evgnort
                    03.10.2024 13:55

                    Что-то как-то сумбурно получилось

                    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. evgnort
              03.10.2024 13:55

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

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

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

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

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

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

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


    1. VoodooCat
      03.10.2024 13:55

      Обращение за границами массива это не UB


  1. a-tk
    03.10.2024 13:55
    +2

  1. mynameco
    03.10.2024 13:55

    А я подумал о том, что они только только убрали из языка всякие not, or, а в другие языки их стали завозить, вмести со всякими сопоставлениями с шаблонами.


  1. fiego
    03.10.2024 13:55

    Именованные параметры зацепили, остаётся, правда, вопрос, не мешает ли это оптимизации? Так-то параметры могут быть в регистрах, а упакованные в структуру уже не факт.


    1. Gumanoid
      03.10.2024 13:55

      Структуры тоже передаются через регистры, если размер поля не больше 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


      1. a-tk
        03.10.2024 13:55

        А как быть с архитектурами, где из регистров только аккумулятор на 8 бит?


        1. Gumanoid
          03.10.2024 13:55

          Для них ничего не поменяется от использования структуры.


      1. VoodooCat
        03.10.2024 13:55

        Нормальные компиляторы передают и 16 байтовые структуры через регистры. С чего вы решили что ваша ссылка актуальна для всех?


        1. Gumanoid
          03.10.2024 13:55

          Да, 8 байт это размер 1 поля, а не всей структуры.


        1. ZyXI
          03.10.2024 13:55

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


  1. Melirius
    03.10.2024 13:55

    Вах, какая прелесть для обфускации :)


  1. unC0Rr
    03.10.2024 13:55

    Не понял, откуда взялась ".z" в параграфе "Выделенный инициализатор"?


  1. Alex_v99
    03.10.2024 13:55
    +1

    Вот что-то вообще не понимаю как такая магия происходит:

    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;


    1. Serpentine
      03.10.2024 13:55
      +1

      В Си имя функции может использоваться как указатель на эту же функцию, по сути имя само представляет собой адрес.

      Например, функция 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. Gryphon88
    03.10.2024 13:55

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

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

    2. sizeof

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

    3. макросы

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


  1. perfect_genius
    03.10.2024 13:55

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