Комментарии 80
Обо всех проблемах, неточностях, опечатках и прочем, что сумело спрятаться от меня в этой простыне, прошу сообщать в ЛС :). Спасибо.
Переводите оба тогда, там есть что обсудить и о чем поспорить.
Согласен. Когда читал, тоже соглашался далеко не со всем. Например, про тот же calloc, кажется, что его использование «всегда и везде» может привести к маскированию некоторых ошибок: например, забытую инициализацию какого-то поля структуры ненулевым значением.

Да и «первое правило» весьма сомнительное. Зачем автор целенаправленно его уточнил до «не используйте Си, если не надо»? Ведь это же частный случай более правильной и очевидной вещи: «Для решения задач используйте подходящие инструменты». В большинстве случаев, если использования Си можно избежать, значит, он там изначально и не был нужен.
Там не только с маскированием ошибок проблемы, там integer overflow потенциальный при любом вызове. Если уж советовать замену, то на reallocarray() из OpenBSD, что автор вышеупомянутой статьи и предлагает.
Мне очень понравилась статья. У С++ есть Страуструп, который ездит по миру и в лекциях активно пропагандирует использование современных конструкций С++, таких как unique_ptr. У чистого С же мне такой человек не известен, и потому появлению подобных статей я очень рад.
Он разве лекции по С дает? Даже больше, насколько он вообще активно собственно написанием кода сейчас занимается? И насколько он реально качественный. Гугл мне не помог с ответом.
На мой взгляд очень спорное решение

void processAddBytesOverflow(void *input, uint32_t len) {
uint8_t *bytes = input;

for (uint32_t i = 0; i < len; i++) {
bytes[0] += bytes[i];
}
}
Повышаются шансы сделать опечатку на вызывающей стороне.
Вместо поля uint8_t* случайно передать соседнее поле SomeClass*
Какой класс? Это же С. К тому же, как быть если вам надо передать именно поле SomeClass?
Какой класс? Это же С

ну ок, пусть будет struct SomeClass* :)

К тому же, как быть если вам надо передать именно поле SomeClass?

просто сделать каст.
когда для суммирования всех байт с накоплением результата в первом байте передаётся адрес структуры, это наиболее вероятно опечатка, чем реальный кейс.
Речь в статье идёт именно о случаях, когда можно передать любые данные, функции типа memcpy(), где операции производятся над памятью, независимо от структуры и это реальный кейс. В остальных случаях естественно надо указывать именно те типы, которые вы хотите использовать. Другие примеры подобных функций: send, encrypt, md5sum и тп.
Согласен, если это просто кусок памяти с размером.
Значит, пример был неудачный, т.к. в передаваемом указателе на память не все байты равнозначны, первый имеет особенное значение.
На самом деле, совет стоящий. Я, к сожалению, когда писал на C, много чаще чем мне хотелось бы сталкивался с сигнатурами вида:
void foo(char* data, int len);
void bar(uint8_t* data, unsigned len);

Вместо каноничного
void baz(void* data, size_t len);

Почему второй вариант лучше?
  1. По такой сигнатуре сразу видно, что принимается просто кусок памяти определенного размера. Понять эту сигнатуру по-другому просто невожможно.
  2. Вам не надо каждый раз кастовать данные к типу первого параметра.

По такой сигнатуре сразу видно, что принимается просто кусок памяти определенного размера. Понять эту сигнатуру по-другому просто невожможно.
Сильно зависит от функции. Для ф-ций с названиями foo, bar совершенно не очевидно, что они делают. К примеру, strchr(char*, int) принимает кусок памяти?
В этом и проблема с функциями вида (char*, int) — непонятно, принимают они просто кусок памяти, или строку. Но в данном случае, учитывая что в названии функции есть str, я бы предположил что именно строку. А в случае, если бы там было (void*, size_t) — тут без вариантов, только кусок памяти.
Во второй можно передать указатель на что угодно, с каким угодно типом и размером типа. По параметру void* не понять, сигнатуру функции это может привести к тому, что в функцию передадут int* и len в виде количества тех самых int.

Опять же передав на вход int8_t мы получим на выходе не то, что ожидали, в виду беззнаковой математики.

Видимо пример не удачный, если кусок памяти и его длина в байтах, тогда void* проблем не создаст.
Во второй можно передать указатель на что угодно, с каким угодно типом и размером типа.
Так в этом и суть объявления. Функции неважно, какой тип. Пример обычный, суть в том, что функция проводит операции над памятью и ей не важен тип. Суть совета в том, что когда вы хотите объявить такую функцию, то иногда может возникнуть соблазн написать uint8_t* — как бы массив байт. Автор говорит о том, что такой подход заведомо неверный и надо писать void*.

Например, если вы хотите объявить какую-то свою memcpy то следует в сигнатуре указывать не uint8_t* (по сути, байтовый массив), а void*.
Вообще сам код говорит, что ему важно какой тип. Ему нужны байты. Так и передавайте байты, а не указатель на ничего. Мне как раз запись вида void* всегда не нравилась. Функция ожидает кусок памяти, память у нас считается в байтах. Так с какого она прячет свои намерения за каким-то непонятным void*? И то что надо делать каст скорее как раз плюс, потому что четко показывает намерение. И практически во всех современных API дело обстоит именно так. Взять тех же ближайших родственников ObjC и С++
C этим подходом есть несколько проблем:
1. В C нет типа byte. Вместо него есть множества других типов: char, signed char, unsigned char, uint8_t, int8_t и тд и тп.
2. void* — это не «указатель на ничего», как вы выразились, а банально адрес в памяти. Такая сигнатура показывает, что функции не важен тип данных. Функции не нужны байты, как вы говорите, ей нужна память. Представление ей не важно. Например, ей не важна знаковость типа, которая иначе всегда вылазит.

Так с какого она прячет свои намерения за каким-то непонятным void*
Как-раз таки своим void* функция и сообщает нам о своих намерениях. Им она говорит пользователю: «Мне нужна память и мне плевать что там у тебя. Просто дай мне адрес.» Хочу также добавить, что тип void* известен и понятен любому, кто хоть немного знает C и не я не понимаю, почему вы считаете его «каким-то непонятным».
memcpy не нужны байты? malloc не выделяет байты? Не надо самого себя обманывать. Нужна всем этим функциям конкретная информация из этого указателя. Вы длинной то что передаете? Правильно, сколько байтов. Ваш же пример — функция работает с байтами, она делает каст даже специально внутри себя. То что вы рекомендуете в этом случае void* это не более чем дань тому, что принято в С. По всем остальным объективным причинам там должен быть массив char, uint8_t или еще чего, как это сделано у других, кто смог позволить себе уйти от С прошлого.

то хоть немного знает C и не я не понимаю, почему вы считаете его «каким-то непонятным

То что он известен и понятен не делает его заведомо хорошим и правильным. И аргумент в пользу void* тогда должен звучать как «так причин и известно остальным, так принято в языке», а не выдумать аргументы, который, по сути, ложны.
Ну, те же функции типа memcpy кастуют этот указатель обычно к машинному слову, то есть чему-то типа int*. Да и многие другие функции кастуют такую память вовсе не к массиву байт. Но даже если бы я и хотел принять байты в открытом виде у меня всё ещё возникла бы проблема — в C нет типа для байтов. Массив char — вообще худшее что можно сделать — в зависимости от платформы и настроек компилятора он может быть либо знаковым либо беззнаковым. А uint8_t — это беззнаковый тип, о котором известно только что он занимает не менее 8 бит. На какой-нибудь платформе, где байты содержат меньше бит (например 7) это будет не однобайтовый а двухбайтовый тип.
Вот и опять пришли к выводу — void* не потому что так правильно, а потому что того требует совместимость с различными платформами и языками.

А uint8_t — это беззнаковый тип, о котором известно только что он занимает не менее 8 бит. На какой-нибудь платформе, где байты содержат меньше бит (например 7) это будет не однобайтовый а двухбайтовый тип.

Вообще стандарт четко дает понять, что char всегда будет равен байту. Более того, тот же gnu C говорит о 8 битах. Как это будет реально работать на таких странных железках я не знаю. Что до использования char, то стандартная библиотека С++ как раз его и использует при работе с теми же файлами, где нас четко просят массив char и его длину.
Что до использования char, то стандартная библиотека С++ как раз его и использует при работе с теми же файлами, где нас четко просят массив char и его длину.
Давайте не смешивать всё-таки C и C++. C++ может себе позволить просить массив CharT (не char, как все прекрасно знают!!!), в C жизнь устроена иначе.

Вот и опять пришли к выводу — void* не потому что так правильно, а потому что того требует совместимость с различными платформами и языками.
Это бессмысленная софистика. Обсуждать как и когда описывать параметры функции не упоминая того, о каком языке идёт речь — это, конечно, отличное развлечение для философов, но программистам обычно нужна конкретика.
программистам обычно нужна конкретика.

И тут внезапно мы используем void* там, где должен быть массив char или unsigned char — при записи в теже потоки мы таки не адрес хотим, а блок байтов. Вот и я хочу конкретики — memcpy копирует мне байты, так пусть она у меня байты и просит. Хорошо что современные API уходят от этих ужасов и даже полностью совместимый Objective C использует для тех же потоков тип uint8_t.
Еще раз, я могу понять, что С это нужно, потому что там жизнь сложна. Но тогда и аргумент в пользу должен быть соответствующий, а не void* потому что функции не важен тип, когда эта функция пишет байты в файл или сокет.
при записи в теже потоки мы таки не адрес хотим, а блок байтов.
Это неверно. При работе с потоками мы манипулируем символами, которые не связаны напрямую с байтами и могут быть любого размера. Вы почему-то перешли от C к C++ и от памяти к операциям ввода/вывода. Я теряю нить спора.

Вот и опять пришли к выводу — void* не потому что так правильно, а потому что того требует совместимость с различными платформами и языками.
Именно потому, что так правильно. Совместимость — один из критериев корректности кода, но это лишь одна причина использовать void*. Вам привели уже десяток аргументов, но вы всё ещё считаете правильным другой вариант по неясным для меня причинам.

Вот и я хочу конкретики — memcpy копирует мне байты
Да не копирует он байты. Он машинными словами копирует, я уже говорил. А если вы имеете ввиду именно физический смысл происходящих процессов, то тогда любая функция превращается в операцию над байтами. Давайте везде просить массивы байт? А что, в итоге-то процессор всегда ими оперирует. А можно просить массив битов. Тоже с физической точки зрения верно — memcpy же скопирует вам биты.
Вот и я хочу конкретики — memcpy копирует мне байты, так пусть она у меня байты и просит.
Хотите конкретики? Её есть у меня.

Рассмотрим простейшую функцию использующую ваш любимый memcpy.

Ну, скажем, такую:
bool fsignbit(float f) {
  int32_t x;
  static_assert(sizeof x == sizeof f, "int32_t and float must have identical size");
  memcpy(&x, &f, sizeof x);
  return x & 0x80000000;
}


Можете показать тут ваши любимые «байты» и объяснить что тут происходит? Я — могу: копируется один объект в другой (это, кстати, единственный способ это сделать переносимо), дальше мы интерпретируем структуру IEEE 754 числа и получаем ответ. При этом нас вообще не волнует тот факт, что на 68K мы берём первый байт, а на X86 — последний. Нам это не нужно. Если стандарт IEEE 754 поддерживается — ответ будет верен. А как вы происходящее будете объяснять со своим подходом?

P.S. Версия для C, понятно, обойдётся обычным assert'ом, не static, но суть дела это не меняет. memcpy не работает с «байтами». Она работает с «кусками памяти». А уж что там внутри у них — это не её собачье дело. То же самое — всякие sha512 и прочие. Вот ICU — та да, работает с байтами (при использовании UTF-8, по крайней мере). А всякие низкоуровневые функции — нет, это не их забота!
По всем остальным объективным причинам там должен быть массив char, uint8_t или еще чего
Вот с этого момента — поподробнее. У вас там «массив char», «uint8_t» или «ещё чего»?

Впомните про пассаж либо вы включаете -fno-strict-aliasing, либо вы сможете работать с объектами исключительно в том виде, в котором они создавались из статьи и объясните мне наконец что вы предлагаете использовать вместо void *? Технически там допустимы два типа: char * и void * и из этих двух альтернатив всё-таки void * выглядит предпочтительнее.
Я бы предпочел typedef на unsigned char как это принято у Apple например. Хотя char тоже успешно используется в том же С++ по стандарту. Оба варианта успешно работают и никогда не доставляли проблем будь это x86 или ARM

К слову, насколько знаю, в той же iOS по-умолчанию используется strict aliasing и там uint8_t* повсеместен для куска памяти. Единственный раз, когда меня это ударило больно, это когда я смешивал выравнивание на различные границы в структурах, от чего код падал.
К слову, насколько знаю, в той же iOS по-умолчанию используется strict aliasing и там uint8_t* повсеместен для куска памяти.
iOS может себе это позволить, так как она рассчитана только и исключительно на платформы с 8-битовыми байтами и ни на каких других процессорах работать не может.

Кстати рассказы про 7-битовые байты — это фигня. C их не поддерживает (UCHAR_MAX по стандарту — минимум 255). Но что делать с машинками, где типа uint8_t просто нет? Такие вполне себе существуют: CRAY, к примеру. Там sizeof(char) == sizeof(int) == 1, а в байте — 32 бита.
void* — это не «указатель на ничего», это указатель на кусок памяти без типа, ровно такой, как нам её выделяет malloc() и освобождает free(). В C, вообще говоря, нет типа данных «байт», и даже нигде не написано, что в байте должно быть 8 бит (это написано в стандарте POSIX, но на C можно писать и для архитектур с другим размером байта), и, таким образом, использовать uint8_t в этом месте — совершенно неразумное ограничение как переносимости, как так и удобства использования вашего кода. Понятно, что явное лучше, чем неявное, но в данном случае void* все-таки намного лучше отражает намерения программиста, на мой взгляд.
Вот это единственный реальный аргумент в пользу void* — переносимость на странные платформы. Что до malloc и free — их сигнатура так же не соответствует тому, что она делает. Эти функции выделяю и освобождают память, которая измеряется в байтах. И если для free это вопрос реального удобства, то malloc это реально функция для работы с байтами. Как и все mem* функции. Просто так повелось и пошло поехало, а современный подход использовать именно четкое намерение — функции нужен массив байт, она это и показывает. Но у них и проблем нет — байт всегда байта и в 8 бит
Еще раз, в C никаких байт нет, и потому никакая память в них не измеряется, точка, конец истории.
Если делать функции работы с памятью через uint8_t*, то на некоторых архитектурах с hard-fail на misaligned read, к примеру, на ARM Cortex-M0, у вас процессор будет просто зависать на первом же обращении к такой памяти примерно в 3/4 случаев.
Опять двадцать пять. Вы сигнатуру memcpy видели? Что там 3 аргументом передается? Что в malloc аргументом передается?
Сигнатуры — видел:
void* memcpu(void*, const void*, size_t)
void* malloc(size_t)

Теперь поговорим о байтах. Если вы считаете, что «байт» — это uint8_t, то в общем случае, вы не правы. Если считаете, что malloc выделяет ровно столько «байт», сколько вами запрошено, то вы тоже не всегда правы (это сильно зависит от реализации, может вообще целую страницу выделить).
Все, что вам гарантирует malloc — это то, что вам вернут достаточно большой кусок нетипизированной памяти для хранения указанного числа «байт», размер которых зависит от архитектуры, и что память потом можно будет освободить при помощи free, либо NULL, если выделить не получилось. Обычная malloc из C даже выравнивание не гарантирует, и в POSIX специально пришлось вводить posix_memalign, чтобы не ловить CPU Exception'ы в случайном месте.
Все, что я хочу тут сказать — в языке нет и не было понятия «восьмибитный байт», а само слово «байт» в документации используется в значении «минимальная единица размера типа».
Обычная malloc из C даже выравнивание не гарантирует
Гарантирует: The pointer returned if the allocation succeeds is suitably aligned so that it may be assigned to a pointer to any type of object with a fundamental alignment requirement and then used to access such an object or an array of such objects in the space allocated (until the space is explicitly deallocated).

в POSIX специально пришлось вводить posix_memalign, чтобы не ловить CPU Exception'ы в случайном месте.
В стандарте тоже есть aligned_alloc, но это для другого: для явно типов явным выравниванием (_Alignas или всякие нестандартные SIMD-типы).
Внезапно, отлично, буду знать.
У меня, честно говоря, нет ни malloc, ни free (вместо них AllocatePool/FreePool и AllocatePages/FreePages, первая из которых по умолчанию выравнивает по sizeof(size_t), а вторая — по размеру страницы), но я помню проблемы с выравниванием выделенной malloc памяти при работе с Cortex-M0 в Keil uVision 4, видимо, это был баг в реализации для этих ядер или просто процессор попался неудачный.
Вы сигнатуру memcpy видели? Что там 3 аргументом передается? Что в malloc аргументом передается?
Размеры объектор. В байтах, да. По стандарту — гарантируется, что в нём есть как минимум 8 бит. Но может быть и больше.
Вот про «может быть и больше» я и пытаюсь говорить, но видимо, построил слишком уж категорический императив, прошу пардону.
Семантически void* — указатель на начало участка памяти. uint8_t* — указатель на начало массива uint8_t, char* — на начало строки символов. size_t — штуки, для memcpy и malloc — количество байт. Байт, что отдельно радует, не обязан быть 8 бит.
Для char гарантируется sizeof(char)==1. При этом char может быть как знаковым, так и беззнаковым типом.
Байт, фактически, можно определить как unsigned char, содержащий CHAR_BIT.
CHAR_BIT бит — это минимально возможная часть для чтения на этой платформе и может быть больше, чем например short.

Поэтому, в C, когда нужно сослаться на участок в памяти лучше использовать void* — так Вы точно показываете свои намерения.
При этом функция, приведенная в статье в качестве примера, должна иметь сигнатуру (uint8_t*, size_t), в силу своей реализации.
Зачем сравнивать работающий инструмент с неработающим?

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

Пример номер 1
Файлы, которые случайно оказались одинаковыми не включатся дважды.
$ ls -l
total 12
-rw-r----- 1 khim khim  26 Jan 23 01:28 ns1.h
-rw-r----- 1 khim khim  26 Jan 23 01:28 ns2.h
-rw-r----- 1 khim khim 113 Jan 23 01:28 test.cpp
$ cat ns1.h
#pragma once
class A
{
};
$ cat ns2.h
#pragma once
class A
{
};
$ cat test.cpp
namespace NS1
{
#include "ns1.h"
}
namespace NS2
{
#include "ns2.h"
}
int main()
{
    NS2::A a;
    return 0;
}
$ gcc test.cpp
test.cpp: In function ‘int main()’:
test.cpp:11:5: error: ‘A’ is not a member of ‘NS2’
     NS2::A a;
     ^
test.cpp:11:5: note: suggested alternative:
In file included from test.cpp:3:0:
ns1.h:2:7: note:   ‘NS1::A’
 class A
       ^
test.cpp:11:12: error: expected ‘;’ before ‘a’
     NS2::A a;
            ^
$ gcc --version
gcc (Ubuntu 4.8.4-2ubuntu1~14.04) 4.8.4
Copyright (C) 2013 Free Software Foundation, Inc.
This is free software; see the source for copying conditions.  There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
Пример номер 2
Совместимые, но, тем не менее разные заголовки будут-таки включены дважды

$ ls -l */*.* *.*
-rw-r----- 1 khim eng 30 Jan 23 01:34 installedlib/header.h
-rw-r----- 1 khim eng 30 Jan 23 01:35 libsource/header.h
-rw-r----- 1 khim eng 79 Jan 23 01:35 test.c
$ cat installedlib/header.h
#pragma once

enum { A = 1 };
$ cat libsource/header.h
#pragma once

enum { A = 1 };
$ gcc test.c 
In file included from test.c:2:0:
installedlib/header.h:3:8: error: redeclaration of enumerator ‘A’
 enum { A = 1 };
        ^
In file included from test.c:1:0:
libsource/header.h:3:8: note: previous definition of ‘A’ was here
 enum { A = 1 };
        ^
Пример номер 3
__need_size_t/#include <stddef.h> тоже не работает...
$ cat header.h 
#pragma once

#ifdef NEED_TYPE_A
struct A {
};
#endif

#ifdef NEED_TYPE_B
struct B {
};
#endif
$ cat test.c
#define NEED_TYPE_A
#include "header.h"

#define NEED_TYPE_B
#include "header.h"

int main() {
  struct A a;
  struct B b;
}
$ gcc test.c
test.c: In function ‘main’:
test.c:9:12: error: storage size of ‘b’ isn’t known
   struct B b;
            ^

Увы и ах, но в C отсуствуют нормальная модульность. Заговолочные файлы — весьма убогая вещь, но #pragma once нормальной альтернативой модулям не является…
#pramga once — не замена модулей, а замена конструкции #ifndef XXX / #define XXX / #endif
И тут она очень хороша, потому что исходный вариант заставляет препроцессор парсить весь файл до последнего endif, когда это не нужно.
Если вы когда-либо общались с языками, которые поддерживают модули, то можете знать, что у модулей есть важная особенность: они имеют имя.

Заголовочные файлы — это такие «модули для бедных на основе препроцессора». Вопрос: где у них имя? Файлы с одинаковым именем далеко не всегда являются одним и тем же модулем (к примеру time.h и sys/time.h — это совершенно разные вещи). Более того: как и в настоящих, «продвинутых» языках в «C» один файл может содержать несколько модулей. К примеру
#define __need_size_t
#include <stddef.h>
#undef __need_size_t
и
#define __need_ptrdiff_t
#include <stddef.h>
#define __need_ptrdiff_t

дадут вам разный результат. Про windows.h я вообще молчу.

Таким образом ответ на вопрос таков: так уж получилось, что имя модуля — это как раз вышеуказзанные XXX в вашей конструкции. А при использовании заголовочного файла с #pramga once имени у модуля нет. Со всеми вытекающими.

В сложных, больших, программах #pragma once использовать просто опасно — вы можете нарваться на разного рода проблемы, описанные выше. В маленьких программах же она бессмысленна, они и так быстро компилируются.

Так что всё, что про неё нужно знать — что использовать эту директиву не следует. Как умные люди и делают. Do not use #pragma once, , include guards are portable and seem to be just as efficient even on platforms that do support #pragma once, so there is no reason to use it, etc.
Логично делать
#define __need_size_t
#define __need_ptrdiff_t
#include <stddef.h>

Понятно, нельзя застраховатся от того, что #include был где-то раньше и из-за pramga once его уже не переподключишь, но ведь и нельзя застраховаться от того, что кто-то раньше уже не сделал
#define __need_size_t
#include <stddef.h>

и оно выпадет с error: redeclaration of ...smthg...
у каждого подхода есть свои проблемы.

мы рассмотрели пример 3

В примере 2 можно столкнуться с ситуацией, когда два разных модуля с одинаковым именем используют одинаковый guard symbol, например, гипотетический loctypes.h и LOCTYPES_H_INCLUDED. В этом случае #pragma once отлично справляется без лишних undef или замены метки в h-файле.

И пример 1. если модуль надо включать 2 раза именно по дизайну программы, его просто не надо оборачивать в #pragma once.
У меня были такие примеры, например какой-нибудь эмулятор чипа надо включить дважды с разными настройками, чтобы получить код эмуляции близких чипов, но немного разных. Разумеется, тут не нужен ни guard symbol, ни pragma once.

Про windows.h я вообще молчу.
Была потребность включить Windows.h два раза в одну единицу трансляции? #pragma once не серебрянная пуля, а компромис именно для таких и похожих заголовков.
Логично делать
#define __need_size_t
#define __need_ptrdiff_t
#include <stddef.h>
Это вообще как? Рассмотрите
полный пример

$ cat header1.h 
#ifndef HEADER1_H_
#define HEADER1_H_

#define __need_size_t
#include <stddef.h>

extern size_t h1_size;

#endif /* HEADER1_H_ */
$ cat header2.h 
#ifndef HEADER2_H_
#define HEADER2_H_

#define __need_ptrdiff_t
#include <stddef.h>

extern ptrdiff_t h2_ptrdiff;

#endif /* HEADER2_H_ */
$ cat header3.h 
#ifndef HEADER3_H_
#define HEADER3_H_

#define __need_size_t
#define __need_ptrdiff_t
#include <stddef.h>

extern size_t h3_size;
extern ptrdiff_t h3_ptrdiff;

#endif /* HEADER3_H_ */
$ cat main.c
#include "header1.h"
#include "header2.h"
#include "header3.h"

extern size_t h1_size;
extern ptrdiff_t h2_ptrdiff;
extern size_t h3_size;
extern ptrdiff_t h3_ptrdiff;

int main() {
}
$ gcc main.c
И объясните мне как вы собираетесь разруливать ситуацию с вашими «гениальными» идеями. Мне в файлах header1.h/header2.h/header3.h не включать stddef.h? И требовать чтобы их использующие программы включали? Как вы это себе вообще представляете?

Ведь и нельзя застраховаться от того, что кто-то раньше уже не сделал
#define __need_size_t
#include <stddef.h>
Нельзя. И не нужно. В примере выше три файла: один файл хочет size_t, другой — ptrdiff_t, третий — их сразу оба. При этом если вы в файле header1.h попробуете использовать size_t — компилятор будет ругаться. И с файлом header2.h и ptrdiff_t — то же самое. Попробуйте, проверьте! Я же рассказываю не о теоретических построениях, а том, как жизнь реально устроена!

Конечно в файле header3.h ничего указывать уже не нужно, так как всё захватилось из header1.h и header2.h… ну так именно поэтому в файле library3.c файл header3.h должен идти первым (как все прекрасно знают). Это позволяет отловить подобного рода ошибки. Если в файле нет ни одной переменной или функции (сплошные enum'ы, скажем), то тут будет плохо, да — но так редко случается…

у каждого подхода есть свои проблемы.
Да, конечно. Но так как #pragma once имеет все проблемы, которые имеет нормальные Include guardы и сегодня, сейчас, больше не имеет ни одного преимущества… зачем это всё?

Была потребность включить Windows.h два раза в одну единицу трансляции?
Конечно.

#pragma once не серебрянная пуля, а компромис именно для таких и похожих заголовков.
Увы и ах, но нет, это не серебрянная пуля. Это грабли, которые превращают работу с файлом windows.h в мучения! Вплоть до того, что о проблемах, вызванных использованием этого высера приходится ажно жёлтым цветом писать в документации.

Не нужно рассказывать про удобства национального вида спорта под названием «хождение по граблям». Это больно и неудобно. Лучше ходить по дорогам.

Когда-то, много лет назад, когда компьютеры были маленькими, а заговоловочные файлы (в частности тот же windows.h) были уже большими — в #pragma once был смысл. Я сам помню времена, когда чтение файла windows.h (со всеми зависимостями) занимало несколько секунд!

Если бы этот рассказ про преимущества #pragma once был в статье из журнала начала 90х годов — можно было бы поностальгировать… но статья вроде как говорит про 2016 год, много раз это подчёркивает… сегодня — использовать #pragma once не не нужно. Точка. Выигрыша уже почти нет (во всяком случае замерить его мало кому удаётся на сколько-нибудь современной системе, а про то, сколько времени читаются файлы с флоппика без SMARTDRV все уже, слава богу, давно забыли), а вот проблемы — имеются по прежнему.
Откуда столько ненависти к бедной #pragma :)
Возможно, я что-то делаю не так, но никаких проблем у меня с ней не было.
Нужно просто понимать, как это работает и для заголовков, подразумевающих повторные включения, использовать другой механизм.

Была потребность включить Windows.h два раза в одну единицу трансляции?
Конечно.

Предствляю, какая получилась каша из объявлений и макросов. Мне кажется, можно было придумать решение получше.
От нестандартности всё. Этих #pragma развели три мешка, даже в стандарт добавили в #pragma STDC, а большую часть проблем они если и решают, то только на определенных компиляторах и при определенной фазе луны. От того их и не любят почти все, кто с ними работает.
Как правило их любят те, кто развёл. И не любят те, кому приходится общаться разными системами.

Вот мне тут говорят: «Предствляю, какая получилась каша из объявлений и макросов. Мне кажется, можно было придумать решение получше».

Ну да — можно было. При написании заголовочного файла! А для этого — нужно использовать не #pragma once, а нормальные ifdef'ы. GNU, кстати, тоже неидеально сделан в этом месте: если вам нужен _GNU_SOURCE, а кто-то какой-нибудь до вас уже time.h включил, то фиг вы свою любимую getdate_r увидите. Но то, что кто-то где-то инструмент неправильно применяет — не повод использовать вместо него другой, который не работает совсем! Тем более нестандартный.
Основная причина использовать #pragma once — меньше возможностей сделать ошибку при перемещении файла в рамках ФС, разделении файла на несколько, и так далее. Ошибки, когда у отрефакторенного файла забыли переименовать гард, весьма забавны, и я был свидетелем, как даже весьма опытные программисты тратят часы, чтобы их починить. Ошибок с прагмой я не видел ни разу, хотя кодовая база их использует даже больше, чем гарды.

Видимо, у меня просто не distributed FS.
Вопрос не в «distributed FS», а в том — один у вас проект или несколько.
О чём вообще тут говорить? #pragma once не имеет отношение к стандарту, а значит использовать её (как и всё, что не имеет отношение к стандарту) следует лишь в крайнем случае, когда нет альтернатив. В данном случае альтернатива есть и она работает лучше. Вывод простой — #pragma once — использовать не следует вообще никогда.
Это неверно.

Когда компилятор читает заголовочный файл первый раз, он обязан прочитать и препроцессировать его полность в любом случае. При этом в случае «pragma once» компилятор запоминает: «файл alpha/bravo/delta.h посещён», а в случае ifndef он запоминает «файл alpha/bravo/delta.h посещён и зашищен символом DELTA_H».

Когда компилятор в следующий раз встречает include «alpha/bravo/delta.h» он проверяет наличие символа DELTA_H и если он попределен, то не читает файл совсем, точно также как в случае использования прагмы.

Документация GCC тут
Даже если вы используете MSVC, где подобных оптимизаций вроде нет (по крайней мере Microsoft про них не пишет) на время компиляции на сегодняшних машинах #pragma once практически никак не влияет.

Что и логично: они обрабатываются на первом, препроцессорном, проходе, который вообще самый простой и быстрый из того, что в современном компиляторе есть. Даже синтаксический разбор программы на C++ на порядок (а то и на два) сложнее и дольше. А там ещё и оптимизации…
Не могу воспроизвести ваш пример 1.

Примеры 2 и 3 — ошибки программиста, непонятно, причём тут #pragma once. Совершенно аналогично можно накосячить с include guard'ами.
На одинаковые/разные времена модификации файлов внимание обратили?

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

Пример 3 с "#pragma once" не работает совсем.

Пункт номер ноль при обсуждении любого инструмента — это вопрос: всегда ли он работает корректно при корректном его использовании. Если нет, то уже неважно — как удобно и хорошо её использовать в тех или иных случаях. Конечно удобство важно, котнечно include guard'ы — не идеал. Но они, по крайней мере, работают когда их правильно используют. #pragma once же этот тест проваливает: она может не работать тогда, когда человек, в общем-то, ничего неправильного и не делал.
На одинаковые/разные времена модификации файлов внимание обратили?

А какими они должны быть? Я-то просто один файл написал и cp сделал.

Сделал ради эксперимента cp -ap — да, gcc падает от этого. clang, что интересно, нет.

Если библиотека разработаывается нормальными людьми

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

Пример 3 с "#pragma once" не работает совсем.


С гардами он тоже как-то не очень:

Скрытый текст
bash-4.2$ cat header.h
#ifndef HEADER_H
#define HEADER_H

#ifdef NEED_TYPE_A
struct A {
};
#endif

#ifdef NEED_TYPE_B
struct B {
};
#endif

#endif

bash-4.2$ cat test.c  
#define NEED_TYPE_A
#include "header.h"

#define NEED_TYPE_B
#include "header.h"

int main() {
    struct A a;
    struct B b;
}
bash-4.2$ gcc test.c
test.c: In function 'main':
test.c:9: error: storage size of 'b' isn't known



Всё, перестаём пользоваться?

#pragma once же этот тест проваливает: она может не работать тогда, когда человек, в общем-то, ничего неправильного и не делал.

Соглашусь с вами только в вашем первом примере, и то с той поправкой, что не работает не #pragma once, а её конкретная реализация в конкретном компиляторе. Это повод починить компилятор, а не прагму пинать, ИМХО.
С гардами он тоже как-то не очень:

Всё, перестаём пользоваться?
В таком виде — да, конечно. Можете найти и посмотреть на то, как в GCC сделан stddef.h. Если вы хотите подобные вещи деть, то гарды тоже нужно делать другие: обрамлять только те вещи, которые нужны. В вашем примере — вам будет нужны два гарда. На HEADER_H_TYPE_A и на HEADER_H_TYPE_B.
Кстати, интересно. Тут встречалось упоминание, что есть костыль, который если встречает файл, начинающийся с
#ifndef XXX / #define XXX и заканчивающийся #endif, обработка ускоряется как для #pragma once, т.е. парсинг всего файла не делается. Получается, два гарда в одном файле заставят этот костыль работать некорректно?
Это не «костыль», а «оптимизация». И нет, два гарда ни к каким проблемам не приводят — см. пример stddef.h ещё раз. Этот файл входит в поставку clang'а и gcc, так что будьте уверены — последнее, что мы сможем увидеть, это проблемы с этой оптимизацией, попавшие в релиз.
Не поленился скачать файл, посмотреть почему «оптимизация» его не ломает.
Оказывается, вместо #ifndef _STDDEF_H там применили #if (!defined(_STDDEF_H) ... ) , чтобы «оптимизация» не сработала.
Это уже не каноничные гарды получаются, не засчитано.

А вообще, ИМХО, делать такие хедеры — идиотизм. Тут уж никакие модули не помогут — один из ранних пропозалов на них, что я видел, предполагал изолированное препроцессорное окружение внутри модуля.
Выбирая gcc, важно указывать -std=c99...

В некоторых случаях (google nacl newlib/glibc) лучше будет -std=gnu99, иначе становятся недоступными POSIX расширения, такие как strdup и др.

Пару слов о портабельности и замечательном компиляторе MSVC, который так несправедливо упустили из повествования
— если MSVC достаточно свежий (>=13), то поддержка c99 там хоть какая-то, да есть
— если MSVC более старый, то можно использовать трюк: на все сишные единицы компиляции вешаем свойство «язык С++» и получаем возможность собирать Си код.
Пример для cmake:
set_source_files_properties(${SOURCES} PROPERTIES LANGUAGE CXX)

Конечено, есть и минусы — придется таскать за собой stdbool.h, статическая инициализация именованных полей структур перестанет работать, в switch case нельзя будеть объявлять переменные (прыжок через инициализацию) и т.п.
Так же к слову о портабельности — форматный спецификатор "%zd" в MSVC реализации стандатной библиотеки не поддерживается.
Особенно радует пассаж "В С99 появились массивы переменной длины (в С11 их можно выбирать по желанию)".

Создаётся впечатление, что у разработчика на «C» в C11 появился выбор: использовать массивы переменной длины или нет. Нет, ребятки: выбор-то появился не у разработчка! А как раз у разработчика компилятора. И на некоторых популярных платформах их таки и нету…

Рекомендовать использовать VLA вместо alloca — это просто издевательство. Особенно в статье, где ещё и #pragma once рекламируется: #pragma once может быть полезна под Windows с MSVC (хотя и там пользы немного), но вот VLA там как раз и нету, так что… где же полный набор этих «вредных советов», чёрт побери, автор предлагает применять?
Статья очень сильно долбанутая. Очень много спорных мест. Но больше всего выбесил unsigned во всех проявлениях. Беззнаковые нельзя использовать не потому, что unsigned long long выглядит глупо, а потому что математика с unsigned делает совсем не то, что ожидает здравый смысл, и более того, это даже зависит от битности процессора. Но в статье огромная куча ссылок на использование unsigned типов. Для тех, кто не понимает этого тезиса — добро пожаловать в реальный мир, пройдите этот тест по теме и порадуйтесь blog.regehr.org/archives/721
Хороший тест. Хорош тем, что показывает, что unsigned использовать просто необходимо — иначе любая мелочь приводит к неопределённому поведению. Написать какую-нибудь распаковку битового потока (типичнейшая задача) на знаковых числах, наверное, можно, но это будет выглядеть, в лучшем случае, как упорная борьба с созданными себе самому трудностями.
unsigned использовать просто необходимо

Всегда по-разному. Недавно ловил ошибку в
for (size_t i = 0; i <= textlen - stringlen; i++)
из-за того, что разность получается отрицательной (textlen и stringlen тоже size_t, как положено). Беглая замена типа i на ptrdiff_t не помогает, т.к. сравнение signed с unsigned делается как unsigned. В итоге наплевал на красоту и сделал
for (size_t i = 0; ptrdiff_t(i) <= ptrdiff_t(textlen - stringlen); i++)
не хочется выносить эту проверку в особый случай.
этот цикл ищет строку в тексте и перебирает возможную позицию начала строки. если строка длиннее текста, должно быть 0 итераций
Но это как раз особый случай. Его и компилятор всё равно рассмотрит отдельно, и всякие инварианты цикла в нём нарушаются.
Но может быть, я бы его записал как
for (size_t j = stringlen; j <= textlen; j++)
— выхода из диапазона unsigned бы не было.
В принципе, красиво. Но тут j — это конец строки в тексте, когда i — был началом, придётся немного поломать внутри цикла…
Другой вариант — записать условие в виде
i+stringlen<=textlen
Но это если не страшна некоторая потеря производительности.
Я там выше выразился конечно же не корректно. unsigned вполне нужно использовать, но там, где это нужно, например, битовые поля, да. Но очень, ну очень много людей используют unsigned переменные просто из соображений, что вот конкретно тут отрицательного числа не может быть, и вроде логично сделать этот счетчик unsigned. И дальше это где-то сравнивается с signed с удивительным результатом. Я вот только про это использование. Это существенный ляп, в который влетают вполне себе опытные люди. В одном из выступлений на cppcon 2014 года Майерс (просмотрел их все, сейчас на какое-то конкретное выступление ссылку не дам, просто запомнился момент) на вопрос про signed/unsigned, почему размер контейнера в STL возвращается как size_t, не нашел ничего лучшего, как сказать «we were young».
Единственный продукт, который в 2016 году позволит форматировать продукты, разработанные на языке С, — clang-format.

GNU indent смотрит на вас с недоумением.

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

Информация

Дата основания
1999
Местоположение
Россия
Сайт
invs.ru
Численность
51–100 человек
Дата регистрации

Блог на Хабре