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

Как я пишу на C по состоянию на конец 2023 года

Уровень сложностиСредний
Время на прочтение9 мин
Количество просмотров27K
Всего голосов 39: ↑27 и ↓12+25
Комментарии91

Комментарии 91

Теперь думаю, что включение const в C было ошибкой, которая дорого нам обошлась.

То есть вы не видите разницу между


void f(const int*);

и

void f(int*);

А есть ещё статические структуры данных -- например, таблицы коэффициентов или ещё что-нибудь в этом роде. На ПК особой разницы нет, объявлены они как просто переменные или как const (ну, не считая того, что без const компилятор не сможет обнаружить ошибочную запись в них -- но будем считать, что проблемы отладки нас не волнуют). Но для всяких там микроконтроллеров разница будет очень существенной: если компилятор видит const, он эти данные положит во флэш-память (в ПЗУ), и они будут готовы к использованию мгновенно, ну а если он его не видит, он будет выделять место в ОЗУ и тем или иным способом заносить туда нужные значения уже во время выполнения программы. В результате программа займёт больше места и будет медленнее работать -- и это не всегда мелочь. Скажем, у популярных "ардуинок" на Атмеге-324 (если память не изменяет) имеется 32 Кбайта флэш-памяти и всего 2 Кбайта ОЗУ.

Справедливости ради, в той же ОС "Linux" константы (например, строковые литералы) размещаются в защищенных от записи страницах памяти. Обращение к ним может привести к ошибкам сегментации. Как я понимаю, это позволяет при запуске нескольких экземпляров одного процесса использовать общие данные без риска их повреждения.

Всё равно, пишете вы так char *str = "Hello World"; или так const char *str = "Hello World"; компилятор положит строки в .rodata (при условии, что вы линкеру не сказали этого не делать), а вот если писать так char str[] = "Hello World";, то буфер будет выделен на стеке и строка будет помещаться в него при вызове функции.

Вы правы. Как раз const char *str позволит выловить ошибку на этапе компиляции, а не выполнения. Хотя скорее всего будет предупреждение, что char *str писать можно, но не нужно.

Массив символов скорее всего будет не в стеке, а в области инициализированных данных. А указатель на массив может быть как глобальной переменной. Так и в стеке локальной области видимости.

Если объявить char str[] = "Hello World"; внутри функции, то в стеке. Таки да, будет предупреждение, но всегда можно использовать -Werror.

Извините, но как оно попадёт в стек как не из области инициализированных данных секции .data ?

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

На amd64 с помощью mov. То есть данные хранятся в инструкциях. Вставьте этот пример в godbolt, увидете, что строка преобразуется в число, это число помещается в регистр, а регистр уже помещается в стек. Я с флагом -O3 смотрел, если что.

#include <stdio.h>

void print_hello(void) {
    char str[] = "Hello World\n";
    fwrite(str, 1, sizeof(str) - 1, stdout);
}

int main(void) {
    print_hello();
}

Да, я забыл сказать, что имею в виду amd64. На других архитектурах, например на MIPS, строка может помещаться в секцию .data. Но копирование таки происходит, что с const, что без const даже в таких случаях.

Тоже посмотрел. Идёт копирование. Причём в секции кода.

И ещё добавлю. Для малых масивов идёт заполнение стека прямо из кода, но для длинной строки (набил пару сотен символов) - вызывается memcpy.

Интересная ситуация получается.

  1. Для коротких строк стек заполняется инструкциями.

  2. Если строка увеличивается, то она переезжает в секцию инициализированных данных, но используются инструкции копирования (clang вызывает memcpy, а gcc использует команды копирования с префиксом rep).

  3. Модификатор static независимо от длины строки использует секцию инициализированных данных и прямое обращение без копирования.

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

static char str[] = ""; не использует стек, но данные размещаются не в read only секции, потому что это массив, а не указатель на строку, а массивы можно изменять. static char *str = ""; создаст указатель, который будет находится в .data и указывать на неизменяемую строку, но сам указатель где-то в тексте программы можно будет поменять. Так что использовать static для строк всегда не имеет особого смысла.

Переменная static, объявленная внутри функции, от переменной (хоть static, хоть не-static), объявленной на глобальном уровне, отличается только областью видимости: только изнутри внутри функции. Однако память для таких переменных выделяется, как для глобальных, т.е. не в стеке, а в обычной области данных, создаваемой в процессе компиляции. Это связано с временем жизни переменной: static должна сохраняться всё время жизни программы, а не только до конца выполнения функции (или даже до выхода из блока внутри функции, где она объявлена). Так что любые, по сути, константы достаточно крупного размера стоит делать именно статиками -- чтобы они создавались один раз (при компиляции или, возможно, в момент запуска программы), а не при каждом выполнении функции.

Так что любые, по сути, константы достаточно крупного размера стоит делать именно статиками

Не совсем так. Любая глобальная переменная, хоть static, хоть не static, будет жить всё время исполнения программы. Однако static переменная доступная только в том .c файле, в котором объявлена, а не static доступна из любого места. Надо учитывать этот момент.

UPD:

Разница между

void somefunc(void) {
    char *str = "очень длинная строка";
    // как-то используем str
}

и

static char *str = "очень длинная строка";

void somefunc(void) {
    // как-то используем str
}

Только в том, что str во втором случае будет доступен и другим функциям. То есть очень большую строку можно поместить и в тело функции, если она больше нигде не используется, так как она всё равно будет храниться в .rodata. А static её имеет смысл объявлять только тогда, когда ещё каким-то другим функциям нужен к ней доступ

Это да, но в данном случае я сравнивал переменные, объявленные внутри функции (где static определяет время жизни), с переменными вне функции (где время жизни от наличия или отсутствия static не зависит) -- ведь обсуждение идёт про создание и инициализацию, что зависит от времени жизни, но не от области видимости.

Я уточнял конкретный тезис

Так что любые, по сути, константы достаточно крупного размера стоит делать именно статиками

С остальным не спорю

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

Если сами исходные данные в процессе выполнения функции меняются -- да. Но если они являются, по сути, константами (те же строковые литералы) -- нет, и их копирование является излишним.

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

Копирования не будет, если объявлять указатель, а не массив.

так char str[] = "строка"; будет копирование, а так char *str = "строка"; - нет. Так и должно быть, ведь для инициализации массива на стеке нужно копирование.

Про это я знаю. С этого начиналась ветка. Про то что при объявлении литерала как неизменного массива на стекле будет всегда происходить копирование - не задумывался. Теперь буду это учитывать.

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

Но, в Вашей крайней статье на этом сайте приведён код:

int hash = calc_hash((unsigned char*)argv[1]);

if(hash != 152) {

char failure[] = "Password is wrong\n";

write(1, failure, sizeof(failure));

return 1;

}

char success[] = "Password is right!\n"; write(1, success, sizeof(success));

И в коде строковые константные литералы в виде массивов.

Я это осознанно сделал, так как строки будут храниться в коде, а код будет шифроваться AES. Это было нужно для усложнения реверс-инженеринга

Понял. Я был не прав. Для меня программирование это хобби. Будет стимул изучить эту тему. Даже не буду задавать вопросы про технические подробности.

В Си++ точно не в стеке. литералы интернализируются. По Си нужно курить стандарт

godbolt показывает обратное

а) про показывают реализациии - другой вопрос

б)GCC преспокойно интернализирует:

.LC0:

        .string "Long enough string"

main:

        push    rbp

        mov     rbp, rsp

        mov     QWORD PTR [rbp-8], OFFSET FLAT:.LC0

        mov     eax, 0

        pop     rbp

        ret

Оказалось, что зависит от флагов оптимизации и длины строки. Если компилировать с -O0 или -O1 короткую строку, такую как в вашем и моём примерах, то она будет хранится в коде - двигаться в регистр, а регистр будет помещается в стек. Если компилировать с -O2 и выше, то эта же строка помещается в .data. Если длина достаточно большая, то при любом уровне оптмимизации она помещается в .data. Это работает и с g++ и с gcc одинаково. Спасибо, что привели контрпример

НЯП у ардуинок одними и теми же инструкциями нельзя лазить по любой памяти, по флешу одни инструкции, по РАМу другие. И потому там тип указателя в РАМ и тип указателя в флеш совсем разные. Одним const не обойтись.

Да, там гарвардская архитектура, поэтому данные во флэше доступны лишь специально предназначенным для этого командам, а не обычным. Но хватает обычного const -- компилятор сам разберётся, какой код генерить. Хотя из-за гарвардской архитектуры бывают приколы. Скажем, если используешь любую функцию семейства printf, она там предполагает, что строка формата является константой и поэтому лежит во флэше. Обычно, конечно, так и бывает, но, если ты хочешь склепать строку динамически в оперативе, то работать не будет :)

Выглядит как костыли. const char * говорит, что указатель на неизменяемые байтики (функция не должна их менять), а не что те байтики исключительно в флеше могут быть.

А это костыль и есть. В подавляющем большинстве случаев работает нормально, но в определённых ситуациях "первооткрывателя" ожидает сюрприз :) Особенно если человек на ассемблере не писал и не понимает по-настоящему разницу в архитектурах.

Для Arduino Pro Micro, например, недостаточно написать const.

Приходится ещё писать PROGMEM. Подробнее здесь:

https://alexgyver.ru/lessons/progmem/

Ну а это уже формально делает указатель на то и на сё -- полностью несовместимыми.

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

По ссылке выше все пляски с бубном описаны.

Да для всех Arduino, и не только, нужно писать PROGMEM. На тех же ESP тоже надо - для всего с гарвардской архитектурой. Если просто написать const, оно будет при старте кода копироваться в ОЗУ.

Спасибо за замечание. У меня не было возможности много разных Arduino перепробовать, только парочка в наличии есть. В какой-то статье на Хабре кто-то писал, что для какого-то Arduino достаточно static const написать, но я не вдавался в подробности, с какой он архитектурой. Возможно, это было все-таки не про Arduino, либо человек что-то напутал.

На современных системах с фон Неймановской архитектурой неизменяемая память встречается вроде как редко, а вот в ретро-разработке иногда приходилось сталкиваться. Например, const размещает данные в постоянной неизменяемой памяти (rodata) в компиляторах для различных картриджных приставок. Там нужно помнить, что для массивов указателей на const данные const нужно указывать два раза - и в типе указателя, и в содержании. Типа, const unsigned char* const array[] = { .. }. Иначе сам массив указателей пойдёт в data, а не в rodata.

Любой микроконтроллер с архитектурой ARM имеет флэш-память -- которая вполне себе неизменяемая (ну да, в неё можно записать, но путём плясок с определёнными регистрами и т.п., а не простым выполнением команды записи по адресу).

Иногда недостаточно написать const, чтобы константы оказались во флеш-памяти.

Для некоторых Arduino, например, ещё надо написать PROGMEM.

Но замечание абсолютно справедливое.

Это avr-gcc - специфичный хак.

Флеш-память она очень, очень всякая бывает.

Для того, чтобы не засорять пространство имён при использовании windows.h, достаточно вынести платформенно-зависимый код в отдельный .c-файл.

Отличные примеры того как не стоит делать. Спасибо!

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

Частенько для легкой переносимости между 16/32/64х битным кодом. Во времена DOS это было оч актуально. Зачем обзывать все типы своими именами - хз.

Не всегда, не всегда. Например, char, во-первых, не обязан занимать 8 битов -- он должен быть не больше short int, но не более того. А во-вторых, нельзя предсказать, трактуется ли char как знаковое или беззнаковое число. Всё это потенциально создаёт проблемы с переносимостью. Ну а если использовать свой собственный тип, можно решить обе эти проблемы.

НЛО прилетело и опубликовало эту надпись здесь
FcBool FcDirCacheUnlink (const FcChar8 *dir, FcConfig *config)

А можете пояснить, зачем так?

FcBool - явно переопределенный bool, FcChar8 - char*, и используются они именно так. Но понимать, что там происходит, очень тяжело.

Вот здесь рекомендуют вообще не использовать bool, а использовать свой enum, особенно в параметрах функций. Потому как понять, что означает true, а что означает false, иногда ни разу не очевидно. Одно дело, когда это результат выполнения чего-то. А если это задает какое-то условие выполнения, легко запутаться, даже если возможных значений всего два. Лучше задать им явные собственные названия, по которым будет сразу очевидно, что это такое. Если речь про C++, нужно писать enum class для строгой типизации.

Использовать #define для таких вещей -- вообще плохая идея, особенно если использовать С++ с его пространствами имён, а в перспективе и с модулями (ну, они введены в C++20, но до сих пор ещё не полностью работоспособны, особенно если нужна гарантированная переносимость между разными компиляторами). Для определения типа всё ж правильней использовать typedef (ну, ещё using, если речь о C++).

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

Что касается, например, стандартных типов вроде uint8_t, с ним есть две с половиной проблемы. Первая: их существование не гарантировано -- во всяком случае, в C++. Как утверждает cppreference.com (в сам стандарт мне смотреть лениво), "signed integer type with width of exactly 8, 16, 32 and 64 bits respectively with no padding bits and using 2's complement for negative values (provided if and only if the implementation directly supports the type)". Есть ещё два семейства вида int_fast8_t и int_least8_t -- они существуют всегда, начиная с C++11, но не гарантируют точный размер (плюс, как уже говорил, может возникнуть ситуация, когда приходится компилировать, используя более древнюю версию стандарта).

Вторая проблема: логическое назначение типа. Если я вижу char, я предполагаю, что имею дело с символом или строкой символов, если вижу int -- что с целым числом или массивом чисел. Что технически это одно и то же (при одинаковых размерах, естественно), в данном случае неважно: правильное название типа помогает лучше понять, для чего служит переменная или параметр.

Ну и полпроблемы: лично меня раздражают слишком уж длинные имена и _t в конце, хотя столь короткие имена, как автор статьи, я не использую: у меня int8, uint8, char8 (который всегда беззнаковый), ну и т.д. и т.п.

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

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

typedef это создание псведонима, можно на все использовать.

В плюсах есть каноничный using для подобного.

Вы правы.

Когда типы по смыслу - это может быть подзказкой (см. линуксовые маны). А вот если uint8_t в u8... я тут готов присодиниться к асфикциации работе с сотрудником.

Я согласен с двумя вещами: хранить длину строки вместе со строкой и возвращать из функции структуру, содержащуюю результат и код ошибки, если ошибка может произойти

Не очень понимаю, при чем тут 2023? Все это можно было делать с С99 (из-за stdint), а остальное еще раньше.

Самые веселые define, что можно придумать. exception прекрасен!
// Подключение сети имён
#define куярга using
#define исемнәр namespace


// Ввод-вывод
#define Татарлар std

#define карагыз cout
#define тәртипсез cerr
#define әйтегез cin
#define икесен_алып_эшләргә swap

#define юл_бетте endl


// Татарские цифры
#define нуль 0
#define бер 1
#define ике 2
#define өч 3
#define дүрт 4
#define биш 5
#define алты 6
#define җиде 7
#define сигез 8
#define тугыз 9
#define ун 10


// Логические/булевые операторы
#define ҺӘМ &&
#define ЯКИ ||
#define һәм &
#define яки |
#define яки_юк ^

#define күбрәк >
#define әзрәк <
#define ул =
#define күбрәк_яки_шул >=
#define әзрәк_яки_шул <=
#define шундый_ук ==
#define шундый_түгел !=

// Объявление переменных
#define башларга int main
#define сан int
#define нокталы float
#define ике_нокталы double

#define бер_яклы unsigned
#define кирәкмәгән_нәрсә void
#define компьютерның_көче size_t
#define зур long
#define кечкенә short

#define үзем_эшләдем template
#define исеме_аның typename

#define бирергә return
#define яхшы 0
#define ялгыш 1

#define яңа new
#define кирәкми delete
#define БЕРНИ NULL
#define белмим_кайда nullptr
#define ниндидер random
#define монысына_тимәскә const
#define бетте_баш throw
#define программаң_тупой exception


// Условия, циклы
#define булса if
#define бу_булса else if
#define юк_бит else
#define була ?
#define булганда while
#define бөтенесенә for

#define бетерергә break
#define монысы_кирәкми continue
#define шул_булса switch
#define мәсәлән case
#define юк_бит_шундыйлар default

#define эшлә ()
#define инде ;


// ООП
#define класс class
#define структура struct
#define сан_пар enum

#define аныкы_гына private
#define дусларына protected
#define бөтен_кешегә public

Ссылка на репозиторий проекта

Тут что-то по татарски. Можно толмача?

Можно было бы воспользоваться _Bool, но я предпочитаю придерживаться естественного размера слова, не вдаваясь в его странную семантику. Начинающему читателю может показаться, что я просто «растрачиваю память», когда пользуюсь 32-разрядными булевыми значениями, но на практике это просто не так. Оно находится или в регистре (возвращаемое значение, локальная переменная), либо всё равно будет увеличиваться до нужного размера при помощи заполнителя (поле структуры).

Как-то неубедительно.

Во-1 все современные актуальные процессоры уже имеют регистры ("естественный размер") по 64 бита, давайте может лучше 8-байтный свой "бул" сделаем?

Во-2 выравнивание в структурах будет иметь место только если следующий за байтом член -- не байт. Если подряд 10 байтовых типов -- каждый будет "невыровненным", точнее байты будут лежать подряд с адресами, различающимися на 1.

Ну и ещё, опять же всем современным актуальным процессорам нет разницы, грузить ли в регистр байт или слово, например movzx (amd64), ldrb (aarch64), lb (risc-v), при этом автоматически делается расширение до всей ширины регистра.

Наконец, если реально хочется экономить -- то логично использовать битфилды ( uint32_t variable_name:1 в таком вот духе)

В языке Си изначально не было специального типа bool. Программист в праве выбрать любой тип в зависимости от архитектуры, скорости/компактности хранения данных.

это изначально было больше двадцати лет назад. даже в несвежих CentOS давно есть bool

не двадцать, а пятьдесят. В этом году юбилей был

Это "изначально" закончилось больше двадцати лет назад

Если вам так понятнее

Вы правы, если речь о C++. Там тип bool так сказать, с рождения.

Но в Си (без плюсов) тип _Bool появился как дополнительный в стандаре C99.

И он является расширением, доступным при подключении <stdbool.h>.

Я о С и именно о С99. Не понимаю я людей, которые не юзают bool из <stdbool.h> // зная о его существовании

Во-1 все современные актуальные процессоры уже имеют регистры по 64 бита, давайте может лучше 8-байтный бул сделаем?

Тут Вы не правы. Если говорить о ПК или там планшетах -- да, так оно и есть. Но мир намного шире, и во всяких промышленных и встраиваемых системах вполне себе актуальны и 8-, и 16-, и 32-разрядные ядра, а вот 64-разрядные как раз не особо-то нужны в большинстве случаев (и, более того, часто непригодны вообще -- не из-за разрядности, конечно, а из-за того, что их использование тянет за собой, скажем, Линух с виртуальной памятью, что абсолютно не совместимо с задачами жёсткого реального времени). В частности, те же ARMы всех разновидностей архитектуры ARMv6-M, ARMv7-M, ARMv8-M являются 32-разрядными.

Во-2 выравнивание в структурах будет иметь место только если следующий за байтом член -- не байт. Если подряд 10 байтов -- каждый будет невыровненным.

Строго говоря -- выровненным. На свою естественную границу, конечно, -- 1-байтовую :)

Ну и ещё, опять же всем современным актуальным процессорам нет разницы, грузить ли в регистр байт или слово, например movzx (amd64), ldrb (aarch64), lb (risc-v), при этом автоматически делается расширение до всей ширины регистра.

Сама загрузка -- да, а вот тратить место в кэше под хранение лишних байтов... Кэш-то не резиновый, поэтому отводить под bool 32 или 64 бита вместо 8 -- не шибко хорошая идея, если брать в общем случае. (понятно, что, если структура состоит из кучи 32-разрядных значений и одного-единственного bool, там разницы уже не будет).

Наконец, если реально хочется экономить -- то логично использовать битфилды (`uint32_t:1` в таком духе)

Автор на это намекал, упомянув flags, но не разъяснив вопрос.

Тут Вы не правы.

Я же просто потроллил автора статьи, что мол а давайте тогда уж 8-байтный бул использовать. :) В остальном я конечно же согласен, но автор упомянул десктопную ось, из чего я и сделал вывод про 64 бита.

PS: в Cortex-M есть ровно те же самые ldrb/strb и там тоже нет никакого смысла булевые значения хранить в чём-то больше байта. Зато там есть расширение участка памяти из битов в 32-битные ворды (т.н. bitband), вот это реально могло бы быть причиной юзать 32-битные "булы", если бы не тот факт, что для доступа к биту структуры таким макаром пришлось бы делать сложное вычисление адреса, что свело бы на нет всю экономию. Но если хранится просто массив булов или в некоторых случаях при доступе в регистры периферии есть выигрыш от использования bitband.

Можно сразу на rust перейти:

https://doc.rust-lang.org/book/ch03-02-data-types.html#integer-types

  • Length Signed Unsigned

  • 8-biti8 u8

  • 16-biti16 u16

  • 32-biti32 u32

  • 64-biti64 u64

  • 128-biti128 u128

  • archisize usize

И строки https://doc.rust-lang.org/book/ch08-02-strings.html (указатель на данные на куче, длинна и емкость)

Возвращаемые типы https://doc.rust-lang.org/book/ch09-02-recoverable-errors-with-result.html

Только надо учитывать, что писать на расте не каждый захочет, мне, например, не понравилось

Но невозможно не заметить, что человек упорно изобретает rust.

Не изобретает. Основная фишка раста - borrow checker, а не тип для строк и уж точно не возврат ошибок из функции (это и в го есть, например).

Или плюсы, там тоже есть классы-строки, строковые слайсы (std::string_view), широко применяется возврат структур/классов, есть механизмы для оптимизации этого (copy elision). Но вообще конечно у гражданина есть и, мягко говоря, странноватые идеи, насчет ненужности const например.

Читал, читал, и не понял, табуляция или пробелы ?

Да, оно

ИМХО, называть строковый тип s8 это неверно. Это сразу создает путанницу с типами u8 и i8. Короче, такой минимализм в именовании типов это "too much" (или "too less").

Походу четыре пробела. Даже не два.

Один же, зачем тратить символы

Зачем вы вообще выбрали С, есть же такие занимательные Java, Python и 1С

БАГ!!! sizeof возвращает size_t который в Си беззнаков! А uintptr_t может иметь другой размер (например на RISC).

Самое смешное, это очень древняя система, я ее видел в книге из.... 1987 года. Про интерактивную графику.

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

Ещё со строками хорошая идея, остальные довольно странны

Да, со строками тоже неплохо, но область применимости узковата. Такое нужно только когда мы по процессору упираемся в производительность strcpy () и/или часто нужно делать strlen (). Если такого нет - то уменьшение читабельности уже не оправдывается. Так что в качестве "общего правила" - совсем не катит.
Еще мне assert () через __builtin_unreachable понравилось, я такое через typedef массива с положительной или отрицательной (в зависимости от аргумента assert-а) длиной делаю, а это тоже gcc-изм и вообще коряво. Может, даже перейду на __builtin_unreachable. Но это мелочь, отдельная капля меда в бочке понятно чего.

ivory?

ага!

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

const - по крайней мере в embedded вещь нужная.

Про возврат структур.

Вариант раз:

bool foo(int *dst, int src);

int dst;

if(foo(&dst, src))

{

}

вариант два:

typedef struct

{

bool ok;

int dst;

} EX_DST;

EX_DST ex_dst;

EX_DST foo(int src);

ex_dst = foo(src);

if(ex_dst.ok)

{

}

Мне кажется вариант с возвратом структур по всем параметрам сложнее - и по размеру исходного кода, и объектного.

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