Примитивные типы
Начнём с основ. Я подбираю краткие имена для примитивных типов. Код получается даже ещё более ясным, чем я ожидал, ревью моего кода, как говорят, тоже делать очень приятно. Эти имена то и дело попадаются в программе, поэтому в данном случае краткость только на пользу. Кроме того, теперь я обхожусь без суффиксов _t – оказалось, они мозолят глаза гораздо сильнее, чем я мог бы подумать.
typedef uint8_t u8;
typedef char16_t c16;
typedef int32_t b32;
typedef int32_t i32;
typedef uint32_t u32;
typedef uint64_t u64;
typedef float f32;
typedef double f64;
typedef uintptr_t uptr;
typedef char byte;
typedef ptrdiff_t size;
typedef size_t usize;
Некоторые предпочитают добавлять к знаковым типам префикс s. Я предпочитаю i, плюс, как видите, у меня есть другие варианты применения s. При работе с размерами вариант isize был бы более единообразным, причём, тогда бы не поглощался идентификатор. Но знаковые значения размеров – это образ жизни, мне они нужнее привилегий. usize – это ниша, предназначенная, в основном, для взаимодействия с внешними интерфейсами там, где это может быть важно.
b32 – это “32-разрядное булево значение”, причём, понятно, зачем оно требуется. Можно было бы воспользоваться _Bool, но я предпочитаю придерживаться естественного размера слова, не вдаваясь в его странную семантику. Начинающему читателю может показаться, что я просто «растрачиваю память», когда пользуюсь 32-разрядными булевыми значениями, но на практике это просто не так. Оно находится или в регистре (возвращаемое значение, локальная переменная), либо всё равно будет увеличиваться до нужного размера при помощи заполнителя (поле структуры). Когда это действительно важно, я упаковываю булевы значения в переменную flags, а 1-байтовое булево значение редко бывает важным.
Притом, что кодировка UTF-16 может показаться нишевой, на самом деле это необходимое зло, когда приходится работать с Win32. Поэтому c16 (“16-разрядный символ”) так часто появляется в коде. За основу для него я мог бы взять uint16_t, но, помещая имя char16_t в соответствующую «иерархию типов», я сообщаю отладчикам (в частности, GDB), что в этих переменных содержатся символьные данные. Официально в Win32 используется тип wchar_t, но при работе с UTF-16 мне нравится недвусмысленность.
Вариант u8 – для восьмёрок, как правило, это данные в кодировке UTF-8. Он отличается от byte, представляющего сырой фрагмент памяти, и представляет собой особый псевдонимный тип. Теоретически, это могут быть разные типы с разной семантикой, хотя, я и не знаю, существуют ли (пока?) какие-либо реализации, в которых такое практикуется. Пока речь только о намерениях.
Что насчёт систем, в которых не поддерживаются типы с фиксированной шириной? Это академический вопрос, и на его обсуждение было впустую потрачено слишком много времени. В частности, не стоило уделять столько внимания выделению типа int_fast32_t и другой подобной бессмыслице. Сейчас практически не существует софта, который бы корректно работал в таких системах. Уверен, что никто этот софт не тестировал – поэтому, представляется, что его качество, в сущности, никого всё равно не волнует.
Я не собираюсь использовать эти имена в отдельности, например, в сниппетах кода (за пределами этой статьи). В противном случае потребовалось бы, чтобы из typedefs читатель мог узнать подробный контекст. Это не стоит дополнительных объяснений. Даже в самых свежих статьях я использовал ptrdiff_t вместо size.
Макросы
Вот мой “стандартный” набор макросов:
#define sizeof(x) (size)sizeof(x)
#define alignof(x) (size)_Alignof(x)
#define countof(a) (sizeof(a) / sizeof(*(a)))
#define lengthof(s) (countof(s) - 1)
Притом, что я по-прежнему предпочитаю писать все константы капслоком (ALL_CAPS), я взял на вооружение нижний регистр для тех макросов, что подобны функциям – поскольку в таком виде их удобнее читать. Они не доставляют таких проблем с пространствами имён, как определения других макросов: у меня не может быть макроса под названием new(), а к тому же переменных и полей под названием new, ведь внешне они не похожи на вызовы функций.
Вот какой вид примет мой любимый макрос assert при работе с GCC и Clang:
#define assert(c) while (!(c)) __builtin_unreachable()
Кроме типичных достоинств у него есть и некоторые другие полезные свойства:
- В нём не требуется отдельных определений для отладочных и релизных сборок. Напротив, он контролируется благодаря участию «чистильщика неопределённых поведений» (UBSan), а неопределённое поведение в описываемых состояниях может либо присутствовать, либо отсутствовать. Это определяется, в частности, при помощи фаззинг-тестирования.
- libubsan предоставляет диагностическую распечатку с указанием файла и номера строки.
- В релизных сборках эта информация превращается в действенную подсказку по оптимизации.
Чтобы активировать утверждения в релизных сборках, переведите UBSan в режим прерываний; это делается командой -fsanitize-trap. Затем включите, как минимум, -fsanitize=unreachable. Теоретически, то же самое должно быть достижимо и при помощи -funreachable-traps, но на момент написания данной статьи эта функция не работает, так как повреждена в нескольких последних релизах GCC.
Параметры и функции
Никаких const. Такая константа не играет никакой практической роли при оптимизации, и я не могу припомнить ни одного случая, в котором с её помощью удалось или удалось бы отловить ошибку. Я некоторое время придерживал const в документации по прототипу, но, поразмыслив, решил, что качественных названий параметров вполне достаточно. Отказавшись от const, я заметил, что стал работать гораздо продуктивнее, так как могу не забивать голову лишней информацией, а визуально код также стал выглядеть гораздо чище. Теперь думаю, что включение const в C было ошибкой, которая дорого нам обошлась.
(Одна небольшая оговорка: const мне по-прежнему нравится в качестве подсказки о том, куда ставить статические таблицы в областях памяти, предназначенных только для чтения. Если потребуется, я выброшу const. Важность её теперь минимальная.)
Литерал 0 – для нулевых указателей. Коротко и ясно. Для меня это не новость, в таком стиле я пишу последние лет 7. Теоретически здесь возможны некоторые пограничные случаи, в которых такая практика может приводить к дефектам, и много чернил было пролито на эту тему, но после нескольких сотен тысяч строк кода я такого пограничного эпизода ещё не встретил.
Пользуйтесь restrict при необходимости, но лучше организуйте код так, чтобы такой необходимости не возникало. Например, не выводите параметры в «out» в виде циклов, либо вообще не используйте исходящих параметров (сейчас расскажу об этом подробнее). По поводу inline я не волнуюсь, поскольку, так или иначе, компилирую всё как один трансляционный блок.
Давайте определения типов (typedef) для всех структур. В своё время я стеснялся так делать, но, если полностью убрать из кода ключевое слово struct, то код проще читать. Если это рекурсивная структура, то ставьте предварительное объявление прямо над ней, чтобы в таких полях можно было использовать краткое имя:
typedef struct map map;
struct map {
map *child[4];
// ...
};
Все функции кроме входных точек объявляйте как static. Опять же, когда всё компилируется как один блок для трансляции, нет причин поступать иначе. Вероятно, это ошибка, что в C вариант static не действует по умолчанию, но здесь я утверждать не берусь. Когда мы немного разгребём код, пользуясь краткими вариантами типов, уберём все const, struct, т.д., функция будет отлично умещаться в одну строку с собственным возвращаемым типом. Я обычно разбиваю их, чтобы название функции начиналось с отдельной строки, но необходимости в этом больше нет.
В том коде, что я пишу, я иногда стараюсь убирать static ради простоты, а ещё потому, что вне полного контекста программы это слово, как правило, не имеет значения. Но ниже я всё-таки буду им пользоваться, чтобы акцентировать описываемый стиль.
В течение некоторого времени я писал все имена типов с заглавной буквы, что, фактически, позволяло выносить их в отдельное пространство имён от переменных и функций, но, в конце концов, перестал так делать. Может быть, вернусь к этой идее в будущем.
Строки
Одно из самых продуктивных изменений, удавшихся мне в этом году, заключается в следующем: я смог полностью отказаться от строк, завершаемых нулём – они кажутся мне ещё одной ужасной ошибкой природы – и взять на вооружение вот такой базовый строковый тип:
#define s8(s) (s8){(u8 *)s, lengthof(s)}
typedef struct {
u8 *data;
size len;
} s8;
Я использовал несколько названий для него, но это – моё любимое. Здесь s означает строку, а 8 — кодировку UTF-8 или u8. Макрос s8 (иногда именуемый просто S) обёртывает строковый литерал C, делая из него строку s8. Строка s8 обрабатывается как толстый указатель, передаваемый и возвращаемый копированием. s8 отлично подходит в в качестве префикса функции – в отличие от str, все из которых зарезервированы. Вот несколько примеров:
static s8 s8span(u8 *, u8 *);
static b32 s8equals(s8, s8);
static size s8compare(s8, s8);
static u64 s8hash(s8);
static s8 s8trim(s8);
static s8 s8clone(s8, arena *);
А затем в комбинации с макросом:
if (s8equals(tagname, s8("body"))) {
// ...
}
Здесь есть соблазн воспользоваться элементом гибкого массива, чтобы в одном выделенном участке памяти уложить сразу и массив, и размер. Пробовал. Такая конструкция получается настолько негибкой, что девальвируются любые её возможные достоинства. Давайте, например, рассмотрим, как создавать такую строку из литерала, и как она будет использоваться.
Бывало, мне казалось: «программа настолько проста, что мне не понадобится строковый тип для таких данных». В этом я почти всегда ошибался. Имея строковый тип, я могу яснее мыслить, а это помогает проще структурировать программы. (В C++ аналогичные возможности появились всего несколько лет назад, они реализованы при помощи std::string_view и std::span.)
Для этой структуры данных есть аналог в UTF-16, s16:
#define s16(s) (s16){u##s, lengthof(u##s)}
typedef struct {
c16 *data;
size len;
} s16;
Не то чтобы мне так нравилось приклеивать u к литералу в макросе, лучше я буду подробно выписывать всё это в виде строкового литерала.
Ещё структуры
Ещё одно изменение – я приучился возвращать структуры, а не исходящие параметры. Фактически, это возврат множественных значений, пусть и без деструктуризации. С организационной точки зрения – большая перемена. Например, эта функция возвращает два значения: результат синтаксического разбора и состояние:
typedef struct {
i32 value;
b32 ok;
} i32parsed;
static i32parsed i32parse(s8);
Вас беспокоит «лишнее копирование»? Не волнуйтесь, ведь, благодаря соглашениям об именовании, здесь получается скрытый исходящий параметр, квалифицированный как restrict. Кроме того, в некоторых случаях он встраивается, так что все издержки на возвращаемые значения всё равно оказываются не важны. При возврате в таком стиле меня не так тянет использовать внутриполосные сигналы, в частности, обозначать ошибки специальными null-возвратами, что достаточно туманно.
Кроме того, так я выработал привычку указывать в самом верху функции возвращаемое значение, которое инициализируется в значении «ноль». Например, ok сразу означает false. Затем я использую его со всеми операторами return. В случае ошибки я могу обойтись без проблем, сразу же вернувшись. Если операция выполнится успешно, то перед возвратом ok устанавливается в true.
static i32parsed i32parse(s8 s)
{
i32parsed r = {0};
for (size i = 0; i < s.len; i++) {
u8 digit = s.data[i] - '0';
// ...
if (overflow) {
return r;
}
r.value = r.value*10 + digit;
}
r.ok = 1;
return r;
}
Кроме статических данных я также отказался от всех инициализаторов кроме традиционного инициализатора нуля. (Важные исключения: макросы s8 и s16.). Здесь речь и о выделенных инициализаторах. Вместо этого я теперь пользуюсь инициализацией с присваиванием. Например, вот «конструктор» с буферизованным выводом:
typedef struct {
u8 *buf;
i32 len;
i32 cap;
i32 fd;
b32 err;
} u8buf;
static u8buf newu8buf(arena *perm, i32 cap, i32 fd)
{
u8buf r = {0};
r.buf = new(perm, u8, cap);
r.cap = cap;
r.fd = fd;
return r;
}
Мне нравится, как это читается, и с когнитивной точки зрения этот код тоже легче. Эти присваивания разделены точками, поэтому сразу видно, в каком порядке они идут. Здесь порядок не важен, а в других случаях бывает важен:
example e = {
.name = randname(&rng),
.age = randage(&rng),
.seat = randseat(&rng),
};
Из одного зерна получаем здесь 6 возможных значений для e. Мне не нравится задумываться обо всех этих возможностях.
И напоследок
Старайтесь писать __attribute, а не __attribute__. Суффикс __ избыточен и не нужен.
__attribute((malloc, alloc_size(2, 4)))
При системном программировании под Win32, где, как правило, требуется сравнительно немного объявлений и определений, лучше не включайте windows.h, а выписывайте прототипы вручную, пользуясь для этого собственными типами. Так сокращается время сборки, пространства имён становятся чище, а интерфейсы в программе — аккуратнее custom (больше никаких DWORD/BOOL/ULONG_PTR, только u32/b32/uptr).
#define W32(r) __declspec(dllimport) r __stdcall
W32(void) ExitProcess(u32);
W32(i32) GetStdHandle(u32);
W32(byte *) VirtualAlloc(byte *, usize, u32, u32);
W32(b32) WriteConsoleA(uptr, u8 *, u32, u32 *, void *);
W32(b32) WriteConsoleW(uptr, c16 *, u32, u32 *, void *);
При работе со встроенным ассемблерным кодом внешние фигурные скобки можно приравнивать к круглым. Перед открывающей фигурной скобкой ставьте пробел, точно как в случае с if, а каждое из последующих ограничений начинайте с двоеточия.
static u64 rdtscp(void)
{
u32 hi, lo;
asm volatile (
"rdtscp"
: "=d"(hi), "=a"(lo)
:
: "cx", "memory"
);
return (u64)hi<<32 | lo;
Разумеется, этим стилистические рекомендации не ограничиваются, но именно к этим я пришёл в уходящем году. Если хотите посмотреть большинство из вышеупомянутых вещей на практике в небольшой программе – взгляните на wordhist.c у меня в Гитхабе.
p/s идет Черная пятница в издательстве «Питер»
Комментарии (85)
m0xf
21.11.2023 13:43+9Для того, чтобы не засорять пространство имён при использовании windows.h, достаточно вынести платформенно-зависимый код в отдельный .c-файл.
BenGunn
21.11.2023 13:43+26Отличные примеры того как не стоит делать. Спасибо!
iig
21.11.2023 13:43+12Когда вижу вместо привычных
uint_8, char *, етц
какие-то свои типы - хочется взять ипридушитьспросить - "с какой целью ты это сделал?". И вот он ответ - потому чтоэкономия байтовавтор может.bodyawm
21.11.2023 13:43-1Частенько для легкой переносимости между 16/32/64х битным кодом. Во времена DOS это было оч актуально. Зачем обзывать все типы своими именами - хз.
SIISII
21.11.2023 13:43-3Не всегда, не всегда. Например, char, во-первых, не обязан занимать 8 битов -- он должен быть не больше short int, но не более того. А во-вторых, нельзя предсказать, трактуется ли char как знаковое или беззнаковое число. Всё это потенциально создаёт проблемы с переносимостью. Ну а если использовать свой собственный тип, можно решить обе эти проблемы.
MiraclePtr
21.11.2023 13:43+6Ну а если использовать свой собственный тип, можно решить обе эти проблемы.
Эти "собственные типы" - просто алиасы для существующих. Если вы сделайте #define чего-то как char'а, то вести оно себя будет точно так же как и char с теми же проблемами. Если вы вместо char хотите использовать uint8_t с гарантированным размером и беззнаковостью, то и используйте его, это стандартный тип, никаких алиасов и "собственных типов" не нужно.
iig
21.11.2023 13:43+1FcBool FcDirCacheUnlink (const FcChar8 *dir, FcConfig *config)
А можете пояснить, зачем так?
FcBool - явно переопределенный bool, FcChar8 - char*, и используются они именно так. Но понимать, что там происходит, очень тяжело.
voldemar_d
21.11.2023 13:43+1Вот здесь рекомендуют вообще не использовать bool, а использовать свой enum, особенно в параметрах функций. Потому как понять, что означает true, а что означает false, иногда ни разу не очевидно. Одно дело, когда это результат выполнения чего-то. А если это задает какое-то условие выполнения, легко запутаться, даже если возможных значений всего два. Лучше задать им явные собственные названия, по которым будет сразу очевидно, что это такое. Если речь про C++, нужно писать enum class для строгой типизации.
SIISII
21.11.2023 13:43Использовать #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 (который всегда беззнаковый), ну и т.д. и т.п.
domix32
21.11.2023 13:43+1Не очень понятно чего вы так с плюсами оживились, когда речь именно про C. Дефайнить типы и тайпдефать классы/структуры в плюсах - зло, в C - необходимость.
SIISII
21.11.2023 13:43Дефайнить типы -- в любом случае зло. Ничто не мешает в чистых сях typedef использовать для простых типов, а не только для структур.
MiyuHogosha
21.11.2023 13:43Когда типы по смыслу - это может быть подзказкой (см. линуксовые маны). А вот если uint8_t в u8... я тут готов присодиниться к
асфикциацииработе с сотрудником.
includedlibrary
21.11.2023 13:43+6Я согласен с двумя вещами: хранить длину строки вместе со строкой и возвращать из функции структуру, содержащуюю результат и код ошибки, если ошибка может произойти
Gryphon88
21.11.2023 13:43+1Не очень понимаю, при чем тут 2023? Все это можно было делать с С99 (из-за stdint), а остальное еще раньше.
igor_suhorukov
21.11.2023 13:43+2Самые веселые 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
Ссылка на репозиторий проекта
mpa4b
21.11.2023 13:43+2Можно было бы воспользоваться _Bool, но я предпочитаю придерживаться естественного размера слова, не вдаваясь в его странную семантику. Начинающему читателю может показаться, что я просто «растрачиваю память», когда пользуюсь 32-разрядными булевыми значениями, но на практике это просто не так. Оно находится или в регистре (возвращаемое значение, локальная переменная), либо всё равно будет увеличиваться до нужного размера при помощи заполнителя (поле структуры).
Как-то неубедительно.
Во-1 все современные актуальные процессоры уже имеют регистры ("естественный размер") по 64 бита, давайте может лучше 8-байтный свой "бул" сделаем?
Во-2 выравнивание в структурах будет иметь место только если следующий за байтом член -- не байт. Если подряд 10 байтовых типов -- каждый будет "невыровненным", точнее байты будут лежать подряд с адресами, различающимися на 1.
Ну и ещё, опять же всем современным актуальным процессорам нет разницы, грузить ли в регистр байт или слово, например
movzx
(amd64),ldrb
(aarch64),lb
(risc-v), при этом автоматически делается расширение до всей ширины регистра.Наконец, если реально хочется экономить -- то логично использовать битфилды (
uint32_t variable_name:1
в таком вот духе)vk6677
21.11.2023 13:43-2В языке Си изначально не было специального типа bool. Программист в праве выбрать любой тип в зависимости от архитектуры, скорости/компактности хранения данных.
Arkasha
21.11.2023 13:43+2это изначально было больше двадцати лет назад. даже в несвежих CentOS давно есть bool
hydroargerum
21.11.2023 13:43не двадцать, а пятьдесят. В этом году юбилей был
Arkasha
21.11.2023 13:43Это "изначально" закончилось больше двадцати лет назад
Если вам так понятнее
vk6677
21.11.2023 13:43+1Вы правы, если речь о C++. Там тип bool так сказать, с рождения.
Но в Си (без плюсов) тип _Bool появился как дополнительный в стандаре C99.
И он является расширением, доступным при подключении
<stdbool.h>.
Arkasha
21.11.2023 13:43+1Я о С и именно о С99. Не понимаю я людей, которые не юзают bool из <stdbool.h> // зная о его существовании
SIISII
21.11.2023 13:43+1Во-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, но не разъяснив вопрос.
mpa4b
21.11.2023 13:43Тут Вы не правы.
Я же просто потроллил автора статьи, что мол а давайте тогда уж 8-байтный бул использовать. :) В остальном я конечно же согласен, но автор упомянул десктопную ось, из чего я и сделал вывод про 64 бита.
PS: в Cortex-M есть ровно те же самые ldrb/strb и там тоже нет никакого смысла булевые значения хранить в чём-то больше байта. Зато там есть расширение участка памяти из битов в 32-битные ворды (т.н. bitband), вот это реально могло бы быть причиной юзать 32-битные "булы", если бы не тот факт, что для доступа к биту структуры таким макаром пришлось бы делать сложное вычисление адреса, что свело бы на нет всю экономию. Но если хранится просто массив булов или в некоторых случаях при доступе в регистры периферии есть выигрыш от использования bitband.
Hellpain
21.11.2023 13:43+8Можно сразу на rust перейти:
https://doc.rust-lang.org/book/ch03-02-data-types.html#integer-types
Length Signed Unsigned
8-bit
i8 u8
16-bit
i16 u16
32-bit
i32 u32
64-bit
i64 u64
128-bit
i128 u128
arch
isize usize
И строки https://doc.rust-lang.org/book/ch08-02-strings.html (указатель на данные на куче, длинна и емкость)
Возвращаемые типы https://doc.rust-lang.org/book/ch09-02-recoverable-errors-with-result.html
includedlibrary
21.11.2023 13:43+3Только надо учитывать, что писать на расте не каждый захочет, мне, например, не понравилось
pda0
21.11.2023 13:43-1Но невозможно не заметить, что человек упорно изобретает rust.
includedlibrary
21.11.2023 13:43+1Не изобретает. Основная фишка раста - borrow checker, а не тип для строк и уж точно не возврат ошибок из функции (это и в го есть, например).
KanuTaH
21.11.2023 13:43Или плюсы, там тоже есть классы-строки, строковые слайсы (
std::string_view
), широко применяется возврат структур/классов, есть механизмы для оптимизации этого (copy elision). Но вообще конечно у гражданина есть и, мягко говоря, странноватые идеи, насчет ненужностиconst
например.
checkpoint
21.11.2023 13:43+8Читал, читал, и не понял, табуляция или пробелы ?
checkpoint
21.11.2023 13:43+1ИМХО, называть строковый тип s8 это неверно. Это сразу создает путанницу с типами u8 и i8. Короче, такой минимализм в именовании типов это "too much" (или "too less").
pashigorev
21.11.2023 13:43-1Зачем вы вообще выбрали С, есть же такие занимательные Java, Python и 1С
MiyuHogosha
21.11.2023 13:43БАГ!!! sizeof возвращает size_t который в Си беззнаков! А uintptr_t может иметь другой размер (например на RISC).
Самое смешное, это очень древняя система, я ее видел в книге из.... 1987 года. Про интерактивную графику.
vap1977
21.11.2023 13:43Какой-то список антипаттернов - как делать ни в коем случае нельзя.
Из существенного, случайно затесалась единственная идея, которую поддержу - возврат структур, явно описывающих полный результат выполнения, включая ошибки.
Kelbon
То есть вы не видите разницу между
void f(const int*);
и
void f(int*);
SIISII
А есть ещё статические структуры данных -- например, таблицы коэффициентов или ещё что-нибудь в этом роде. На ПК особой разницы нет, объявлены они как просто переменные или как const (ну, не считая того, что без const компилятор не сможет обнаружить ошибочную запись в них -- но будем считать, что проблемы отладки нас не волнуют). Но для всяких там микроконтроллеров разница будет очень существенной: если компилятор видит const, он эти данные положит во флэш-память (в ПЗУ), и они будут готовы к использованию мгновенно, ну а если он его не видит, он будет выделять место в ОЗУ и тем или иным способом заносить туда нужные значения уже во время выполнения программы. В результате программа займёт больше места и будет медленнее работать -- и это не всегда мелочь. Скажем, у популярных "ардуинок" на Атмеге-324 (если память не изменяет) имеется 32 Кбайта флэш-памяти и всего 2 Кбайта ОЗУ.
vk6677
Справедливости ради, в той же ОС "Linux" константы (например, строковые литералы) размещаются в защищенных от записи страницах памяти. Обращение к ним может привести к ошибкам сегментации. Как я понимаю, это позволяет при запуске нескольких экземпляров одного процесса использовать общие данные без риска их повреждения.
includedlibrary
Всё равно, пишете вы так
char *str = "Hello World";
или такconst char *str = "Hello World";
компилятор положит строки в .rodata (при условии, что вы линкеру не сказали этого не делать), а вот если писать такchar str[] = "Hello World";
, то буфер будет выделен на стеке и строка будет помещаться в него при вызове функции.vk6677
Вы правы. Как раз
const char *str позволит выловить ошибку на этапе компиляции, а не выполнения. Хотя скорее всего будет предупреждение, что char *str писать можно, но не нужно.
Массив символов скорее всего будет не в стеке, а в области инициализированных данных. А указатель на массив может быть как глобальной переменной. Так и в стеке локальной области видимости.
includedlibrary
Если объявить
char str[] = "Hello World";
внутри функции, то в стеке. Таки да, будет предупреждение, но всегда можно использовать-Werror
.vk6677
Извините, но как оно попадёт в стек как не из области инициализированных данных секции .data ?
Я обязательно проверю это. Моё предположение, что в стеке функции будет указатель на начало массива, инициализированный числом из секции .data. Не думаю что будет копирование данных.
includedlibrary
На amd64 с помощью mov. То есть данные хранятся в инструкциях. Вставьте этот пример в godbolt, увидете, что строка преобразуется в число, это число помещается в регистр, а регистр уже помещается в стек. Я с флагом
-O3
смотрел, если что.includedlibrary
Да, я забыл сказать, что имею в виду amd64. На других архитектурах, например на MIPS, строка может помещаться в секцию .data. Но копирование таки происходит, что с const, что без const даже в таких случаях.
vk6677
Тоже посмотрел. Идёт копирование. Причём в секции кода.
vk6677
И ещё добавлю. Для малых масивов идёт заполнение стека прямо из кода, но для длинной строки (набил пару сотен символов) - вызывается memcpy.
vk6677
Интересная ситуация получается.
Для коротких строк стек заполняется инструкциями.
Если строка увеличивается, то она переезжает в секцию инициализированных данных, но используются инструкции копирования (clang вызывает memcpy, а gcc использует команды копирования с префиксом rep).
Модификатор static независимо от длины строки использует секцию инициализированных данных и прямое обращение без копирования.
Задумался, возможно стоит для строковых литералов, инициализируемых внутри функции всегда использовать static...
includedlibrary
static char str[] = "";
не использует стек, но данные размещаются не в read only секции, потому что это массив, а не указатель на строку, а массивы можно изменять.static char *str = "";
создаст указатель, который будет находится в .data и указывать на неизменяемую строку, но сам указатель где-то в тексте программы можно будет поменять. Так что использовать static для строк всегда не имеет особого смысла.SIISII
Переменная static, объявленная внутри функции, от переменной (хоть static, хоть не-static), объявленной на глобальном уровне, отличается только областью видимости: только изнутри внутри функции. Однако память для таких переменных выделяется, как для глобальных, т.е. не в стеке, а в обычной области данных, создаваемой в процессе компиляции. Это связано с временем жизни переменной: static должна сохраняться всё время жизни программы, а не только до конца выполнения функции (или даже до выхода из блока внутри функции, где она объявлена). Так что любые, по сути, константы достаточно крупного размера стоит делать именно статиками -- чтобы они создавались один раз (при компиляции или, возможно, в момент запуска программы), а не при каждом выполнении функции.
includedlibrary
Не совсем так. Любая глобальная переменная, хоть static, хоть не static, будет жить всё время исполнения программы. Однако static переменная доступная только в том .c файле, в котором объявлена, а не static доступна из любого места. Надо учитывать этот момент.
UPD:
Разница между
и
Только в том, что str во втором случае будет доступен и другим функциям. То есть очень большую строку можно поместить и в тело функции, если она больше нигде не используется, так как она всё равно будет храниться в .rodata. А static её имеет смысл объявлять только тогда, когда ещё каким-то другим функциям нужен к ней доступ
SIISII
Это да, но в данном случае я сравнивал переменные, объявленные внутри функции (где static определяет время жизни), с переменными вне функции (где время жизни от наличия или отсутствия static не зависит) -- ведь обсуждение идёт про создание и инициализацию, что зависит от времени жизни, но не от области видимости.
includedlibrary
Я уточнял конкретный тезис
С остальным не спорю
vk6677
В принципе всё логично. Если мы хотим при каждом вызове функции иметь её первоначальное состояние, по придётся каждый раз копировать. Хоть из секции данных, хоть инструкциями кода. Избежать копирования невозможно.
SIISII
Если сами исходные данные в процессе выполнения функции меняются -- да. Но если они являются, по сути, константами (те же строковые литералы) -- нет, и их копирование является излишним.
vk6677
Много статей читал, какие эффективные сейчас компиляторы. Думал, что если строковой литерал не меняется в функции, то будет исключено лишнее копирование. Заблуждался. Буду знать, и сам за счёт ключевых слов заботиться о исключении копирования, где это не требуется.
includedlibrary
Копирования не будет, если объявлять указатель, а не массив.
так
char str[] = "строка";
будет копирование, а такchar *str = "строка";
- нет. Так и должно быть, ведь для инициализации массива на стеке нужно копирование.vk6677
Про это я знаю. С этого начиналась ветка. Про то что при объявлении литерала как неизменного массива на стекле будет всегда происходить копирование - не задумывался. Теперь буду это учитывать.
includedlibrary
Это я к тому, что умный компилятор всё правильно в данном случае делает. Ему сказали сделать массив, он сделал. Если массив при этом не изменяется, то неясно, зачем его вообще объявили, когда нужно было использовать строковый литерал.
vk6677
Но, в Вашей крайней статье на этом сайте приведён код:
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));
И в коде строковые константные литералы в виде массивов.
includedlibrary
Я это осознанно сделал, так как строки будут храниться в коде, а код будет шифроваться AES. Это было нужно для усложнения реверс-инженеринга
vk6677
Понял. Я был не прав. Для меня программирование это хобби. Будет стимул изучить эту тему. Даже не буду задавать вопросы про технические подробности.
MiyuHogosha
В Си++ точно не в стеке. литералы интернализируются. По Си нужно курить стандарт
includedlibrary
godbolt показывает обратное
MiyuHogosha
а) про показывают реализациии - другой вопрос
б)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
includedlibrary
Оказалось, что зависит от флагов оптимизации и длины строки. Если компилировать с -O0 или -O1 короткую строку, такую как в вашем и моём примерах, то она будет хранится в коде - двигаться в регистр, а регистр будет помещается в стек. Если компилировать с -O2 и выше, то эта же строка помещается в .data. Если длина достаточно большая, то при любом уровне оптмимизации она помещается в .data. Это работает и с g++ и с gcc одинаково. Спасибо, что привели контрпример
mpa4b
НЯП у ардуинок одними и теми же инструкциями нельзя лазить по любой памяти, по флешу одни инструкции, по РАМу другие. И потому там тип указателя в РАМ и тип указателя в флеш совсем разные. Одним const не обойтись.
SIISII
Да, там гарвардская архитектура, поэтому данные во флэше доступны лишь специально предназначенным для этого командам, а не обычным. Но хватает обычного const -- компилятор сам разберётся, какой код генерить. Хотя из-за гарвардской архитектуры бывают приколы. Скажем, если используешь любую функцию семейства printf, она там предполагает, что строка формата является константой и поэтому лежит во флэше. Обычно, конечно, так и бывает, но, если ты хочешь склепать строку динамически в оперативе, то работать не будет :)
mpa4b
Выглядит как костыли. const char * говорит, что указатель на неизменяемые байтики (функция не должна их менять), а не что те байтики исключительно в флеше могут быть.
SIISII
А это костыль и есть. В подавляющем большинстве случаев работает нормально, но в определённых ситуациях "первооткрывателя" ожидает сюрприз :) Особенно если человек на ассемблере не писал и не понимает по-настоящему разницу в архитектурах.
voldemar_d
Для Arduino Pro Micro, например, недостаточно написать const.
Приходится ещё писать PROGMEM. Подробнее здесь:
https://alexgyver.ru/lessons/progmem/
mpa4b
Ну а это уже формально делает указатель на то и на сё -- полностью несовместимыми.
iig
Ну там мало что архитектура гарвардская, так и доступ к данным из той памяти с интересным бубном делается. Там нет универсальных указателей.
voldemar_d
По ссылке выше все пляски с бубном описаны.
shiru8bit
Да для всех Arduino, и не только, нужно писать PROGMEM. На тех же ESP тоже надо - для всего с гарвардской архитектурой. Если просто написать const, оно будет при старте кода копироваться в ОЗУ.
voldemar_d
Спасибо за замечание. У меня не было возможности много разных Arduino перепробовать, только парочка в наличии есть. В какой-то статье на Хабре кто-то писал, что для какого-то Arduino достаточно static const написать, но я не вдавался в подробности, с какой он архитектурой. Возможно, это было все-таки не про Arduino, либо человек что-то напутал.
shiru8bit
На современных системах с фон Неймановской архитектурой неизменяемая память встречается вроде как редко, а вот в ретро-разработке иногда приходилось сталкиваться. Например, const размещает данные в постоянной неизменяемой памяти (rodata) в компиляторах для различных картриджных приставок. Там нужно помнить, что для массивов указателей на const данные const нужно указывать два раза - и в типе указателя, и в содержании. Типа, const unsigned char* const array[] = { .. }. Иначе сам массив указателей пойдёт в data, а не в rodata.
SIISII
Любой микроконтроллер с архитектурой ARM имеет флэш-память -- которая вполне себе неизменяемая (ну да, в неё можно записать, но путём плясок с определёнными регистрами и т.п., а не простым выполнением команды записи по адресу).
voldemar_d
Иногда недостаточно написать const, чтобы константы оказались во флеш-памяти.
Для некоторых Arduino, например, ещё надо написать PROGMEM.
Но замечание абсолютно справедливое.
iig
Это avr-gcc - специфичный хак.
Флеш-память она очень, очень всякая бывает.