Критика статьи «Как писать на С в 2016 году»

Original author: Keith S. Thompson
  • Translation
  • Tutorial

От переводчика:

Данная публикация является третьей и последней статьей цикла, стихийно возникшего после публикации перевода статьи "How to C in 2016" в блоге Inoventica Services. Тут критикуются некоторые изложенные в оригинале тезисы и окончательно формируется законченная "картина" мнений о поднимаемых автором первой публикации вопросах и методах написания кода на С. Наводку на англоязычный оригинал предоставил пользователь ImpureThought, за что ему отдельное спасибо. Со второй публикацией, наводку на текст которой дал, как я думаю, знакомый многим, пользователь CodeRush, можно ознакомиться здесь.

Мэтт (на сайте которого не указана фамилия автора, по крайней мере, насколько мне известно) опубликовал статью «Программирование на С в 2016 году», которая позже появилась на Reddit и Hacker News, именно на последнем ресурсе я ее и обнаружил.

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

Я не цитирую всю публикацию Мэтта. В частности, решил опустил некоторые пункты, с которыми согласен. Начнем.

Первое правило программирования на С – не используйте его, если можно обойтись другими инструментами.

С подобным утверждением я не согласен, но это слишком широкая тема для обсуждения.

При программировании на С сlang по умолчанию обращается к С99, а потому дополнительные опции не требуются.

Это зависит от версии clang: clang 3.5 по умолчанию работает с C99, clang 3.6 — с С11. Я не уверен, насколько жестко это соблюдается при использовании "из коробки".

Если вам необходимо использовать определенный стандарт для gcc или clang, не усложняйте, используйте std=cNN -pedantic.

По умолчанию gcc-5 запрашивает -std=gnu11, но на практике нужно указывать с99 или c11 без GNU.

Ну, разве что если вы не хотите использовать конкретные gcc расширения, которые, в принципе, вполне подходят для данных целей.

Если вы обнаружили в новом коде что-то вроде char, int, short, long или unsigned, вот вам и ошибки.

Вы меня, конечно, извините, но это чушь. В частности, int – самый приемлемый тип целочисленных данных для текущей платформы. Если речь идет о быстрых беззнаковых целых, как минимум, на 16 битов, нет ничего плохого в использовании int (или можно ссылаться на опцию int_least16_t, которая прекрасно справится с функциями того же типа, но ИМХО это куда подробнее, чем оно того стоит).

В современных программах необходимо указывать #include <stdint.h> и только потом выбирать стандартные типы данных.

То, что в имени int не прописано «std», не значит, будто мы имеем дело с чем-то нестандартным. Такие типы, как int, long и др., встроены в язык С. А typedefs, зафиксированные в <stdint.h>, появляются позже в качестве дополнительной информации. Это не делает их менее «стандартными», чем встроенные типы, хотя они, в некотором роде, и уступают последним.

float — 32-битный стандарт с плавающей точкой
double — 64-битный стандарт с плавающей точкой

float и double – весьма распространенные IEEE типы для 32 и 64-битных стандартов с плавающей точкой, в частности, на современных системах, не стоит на этом зацикливаться при программировании на С. Я работал на системах, где float использовали на 64 битах.

Обратите внимание: больше никаких char. Обычно на языке программирования С команду char не только называют, но и используют неправильно.

К сожалению, слияние параметров и байтов при программировании на С неизбежно, и тут мы просто застряли. Тип char стабильно приравнивается одному байту, где «байт» — минимум, 8 битов.

Разработчики ПО то и дело употребляют команду char для обозначения «байта», даже когда выполняются беззнаковые байтовые операции. Гораздо правильнее для отдельных беззнаковых байтовых/октетных величин указывать uint8_t, а для последовательности беззнаковых байтовых/октетных величин выбирать uint8_t *.

Если подразумеваются байты, задействуйте unsigned char. Если речь об октетах, выбирайте uint8_t. В случае, когда CHAR_BIT > 8, uint8_t создать не удастся, а, значит, не получится и скомпилировать код (возможно, вам именно это и нужно). Если же мы работаем с объектами, как минимум, на 8 битов, используйте uint_least8_t. Если под байтами имеются в виду октеты, добавляем в код что-то вроде этого:

#include <limits.h>
#if CHAR_BIT != 8
    #error "This program assumes 8-bit bytes"
#endif

Обратите внимание: POSIX запрашивает CHAR_BIT == 8.

на языке программирования С строковые литералы ("hello") выглядят, как char *.

Нет, строковые литералы задаются типом char[]. В частности, для "hello" это char[6]. Массивы не являются указателями.

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

Многим типам на C присваиваются имена, состоящие из нескольких слов. И в этом нет ничего плохого. Если вам лень печатать лишние символы, это не значит, что стоит пичкать код всевозможными сокращениями.

Кому захочется вводить unsigned long long int, если можно ограничиться простым uint64_t?

С одной стороны, вы можете задействовать unsigned long long, подразумевая int. В то же время, зная, что это разные вещи и что тип unsigned long long, как минимум, 64-битный, причем в нем могут присутствовать или отсутствовать отступы. uint64_t рассчитан ровно на 64 бита, причем без битов отступов; данный тип совсем не обязательно прописан в том или ином коде.

unsigned long long встроенный тип на С. С ним знаком любой специалист, работающий с этим языком программирования.

Либо попробуйте uint_least64_t, который может быть идентичным или отличаться от unsigned long long.

Типы <stdint.h> куда конкретнее и точнее по смыслу, они лучше передают намерения автора, компактны – что немаловажно и для эксплуатации, и для читабельности.

Конечно, типы intN_t и uintN_t гораздо конкретнее. Но ведь не во всех кодах это главное. Не уточняйте то, что для вас неважно. Выбирайте uint64_t только тогда, когда вам действительно нужно ровно 64 бита — ни больше, ни меньше.

Иногда требуются типы с точной длиной, например, когда необходимо подстроиться под определенный формат (Иногда делается акцент на порядке байтов, выравнивании элементов и тп.; <stdint.h> на С не предусматривает возможности описания конкретных параметров). Чаще всего достаточно задать определенный диапазон значений, для чего подойдут встроенные типы [u]int_leastN_t или [u]int_leastN_t.

Правильный тип для указателей в данном случае — uintptr_t, он задается файлами <stdint.h>.

Какая жуткая ошибка.

Начнем с мелких погрешностей: uintptr_t задается <stdint.h>, а не <stddef.h>.

Это, если вообще говорить о конкретике. Вызов команды, где void* невозможно преобразовать в другой целочисленный тип без потери данных, вряд ли определяет uintptr_t (Такие случаи встречаются крайне редко, если и вовсе существуют).

Вместо:

long diff = (long)ptrOld - (long)ptrNew;


Да, так дела не делаются.

Используйте:

ptrdiff_t diff = (uintptr_t)ptrOld - (uintptr_t)ptrNew;


Но ведь этот вариант ничуть не лучше.

Если хотите подчеркнуть разницу типов, пишите:

ptrdiff_t diff = ptrOld - ptrNew;

Если нужно сделать акцент на байтах, выбирайте что-то вроде:

ptrdiff_t diff = (char*)ptrOld - (char*)ptrNew;

Если ptrOld и ptrNew не указывают на необходимые параметры, или просто перескакивают с конца объекта, сложно будет проследить, как указатель вызывает команду вычитания данных. Переход на uintptr_t гарантирует хотя бы относительный результат, правда, его вряд ли можно назвать очень полезным. Проводить сравнение или другие арифметические действия с указателями допустимо только при написании кода для систем высокого уровня, в противном случае важно, чтобы исследуемые указатели ссылались на конец определенного объекта или перескакивали с него (Исключение: == и != прекрасно работают для указателей, ссылающихся на разные объекты).

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

А вот и нет. Понятие «равный слову» весьма абстрактно. intptr_t знаковый целочисленный тип, который успешно конвертирует void* в intptr_t и обратно без потери данных. Причем это может быть значение, превышающее void*.

На 32-битных платформах intptr_t трансформируется в int32_t.

Бывает, но не всегда.

На 64-битных платформах intptr_t приобретает вид int64_t.

И снова, вполне вероятно, но не обязательно.

По сути, size_t – что-то вроде «целой величины, способной хранить огромные индексы массива.

Неееет.

а, значит, ему под силу фиксировать внушительные показатели смещения в создаваемой программе.

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

В любом случае на современных платформах size_t обладает, практически, теми же характеристиками, что и uintptr_t, а потому на 32-битных версиях size_t трансформируется в uint32_t, а на 64-битных – в uint64_t.

Скорее всего, но не обязательно.

А если конкретнее, size_t может использоваться для сохранения размера любого отдельного объекта, в то время как uintptr_t задает любое значение указателя, а, соответственно, с их помощью вы больше не перепутаете адреса байтов различных объектов. Большинство современных систем работает с неделимыми адресными строками, и поэтому, теоретически, максимальный размер объекта равен общему объему памяти. Стандарты программирования на С требуют строгого соблюдения данного требования. Так, например, вы можете столкнуться с ситуацией, когда на 64-битной системе объекты не превышают 32 бита.

Выделяя слово «современные», мы автоматически опускаем обе старые альтернативы (вроде x86, на которой использовали сегментированную адресацию с указателями near и far), и не касаемся возможных будущих продуктов, которые также могут предусматривать совместимость со стандартами С, хотя и выходить за рамки определения «современных».

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

Это один из вариантов, но не единственное удачное решение (И, наверняка, вы согласитесь, что нужно все же упоминать void* для "%р").

Исходное значение указателя — %p (в современных компиляторах отображается в шестнадцатеричной системе; изначально отсылает указатель к void *)

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

     printf("Local number: %" PRIdPTR "\n\n", someIntPtr);

Имя someIntPtr подразумевает тип int*, на самом деле задает тип intptr_t.

Тут могут быть вариации на тему, а, значит, вам не нужно заучивать бесконечные комбинации имен макросов:

some_signed_type n;
some_unsigned_type u;
printf("n = %jd, u = %ju\n", (intmax_t)n, (uintmax_t)u);

intmax_t и uintmax_t, как правило, 64-битные. Их преобразования гораздо экономичнее физических I/O.

Обратите внимание: % попадает в тело литерала форматирующей строки, в то время как указатель типа остается за его пределами.

Все это части форматирующей строки. Макросы задаются как строковые литералы, объединенные с соседними строковыми литералами.

Современные компиляторы поддерживают #pragma once

Но никто не говорит, что вы обязаны использовать данную директиву. Даже в инструкции процессоров не озвучиваются подобные рекомендации. И в разделе «Заголовки с Once» ни слова о #pragma once; зато описывается #ifndef. В следующем разделе «Альтернативы упаковщика #ifndef» мелькнула #pragma once, но и в этом случае всего лишь отмечено, что это не портируемая опция.

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

И кто это дает такие рекомендации? Директива #ifndef, может, и неидеальна, зато надежна и портируема.

ВАЖНО: Если в вашей структуре предусмотрены внутренние отступы, {0} метод не обнулит дополнительные байты, предназначенные для этих целей. Так, например, происходит, если в struct thing 4 байта отступов после counter (на 64-битной платформе), потому что структуры заполняются с шагом равным одному слову. Если вам нужно обнулить всю структуру включая неиспользованные байты отступов, указывайте memset(&localThing, 0, sizeof(localThing)), так как sizeof(localThing) == 16 bytes, несмотря на то, что доступно всего 8 + 4 = 12 байтов.

Задача усложняется. Обычно нет никаких причин уделять особое внимание байтам отступов. Если вам все же захотелось посвятить им свое драгоценное время, используйте memset для их обнуления. Хотя отмечу, что очистка структур с помощью memset, даже с учетом того, что целым элементам, действительно, будет присвоено значение нуля, не гарантирует того же эффекта для типов с плавающей точкой или указателей – должны, соответственно, равняться 0.0 и NULL (хотя на большинстве систем функция отлично работает).

В С99 появились массивы переменной длины

Нет, в C99 не предусмотрены инициализаторы для VLA (массивы переменной длины). Но Мэтт, по сути, и не пишет об инициализаторах VLA, упоминая только сами VLA.

Массивы переменной длины – явление противоречивое. В отличие от malloc, они не предполагают обнаружение ошибок при распределении ресурсов. Так что, если вам нужно выделить N количество байтов данных, вам понадобится:

{
    unsigned char *buf = malloc(N);
    if (buf == NULL) { /* allocation failed */ }
    /* ... */
    free(buf);
}

по крайней мере, в общем и целом, это безопаснее, чем:

{
    unsigned char buf[N];
    /* ... */
}

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

Причем со старыми массивами фиксированной длины возникали аналогичные вопросы. Пока вы проверяете размер перед созданием массива, VLA с переменным N так же безобиден, как массив фиксированной длины того же размера. Как правило, для описания массивов фиксированной длины выбирают значение, превышающее количество предполагаемых элементов, поскольку его часть необходима для хранения фактических данных. С VLA можно выделить ровно столько места, сколько требуется компонентам. И здесь я согласен с рекомендацией Мэтта.

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

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

Заведомо ОШИБОЧНО:

void processAddBytesOverflow(uint8_t *bytes, uint32_t len) {
    for (uint32_t i = 0; i < len; i++) {
        bytes[0] += bytes[i];
    }
}

Вместо этого используйте:

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

    for (uint32_t i = 0; i < len; i++) {
        bytes[0] += bytes[i];
    }
}

Согласен, void* идеальный тип для фиксирования параметров произвольного фрагмента памяти. Взять хотя бы функции mem* в стандартной библиотеке (Но len должен быть size_t, а не uint32_t).

Объявив тип исходных данных, как void *, и повторно назначив или еще раз сославшись на фактический тип данных, который нужен прямо в теле функции, вы обезопасите пользователей, ведь так им не придется думать о том, что происходит в вашей библиотеке.

Маленькое замечание: это не прописано в функции Мэтта. Здесь мы видим неявное преобразование void* в uint8_t*.

В этом примере некоторые читатели столкнулись с проблемой выравнивания.

И они ошиблись. Если мы работаем с определенным фрагментом памяти, как с последовательностью байтов, это всегда безопасно.

C99 предоставляет нам весь набор функций <stdbool.h>, где true равняется 1, а false - 0.

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

В случае с удачными/неудачными возвращаемыми значениями функции должны выдавать true or false, а не возвращаемый тип int32_t, требующий ручного ввода 1 и 0 (или, что еще хуже, 1 и -1; как тогда разобраться: 0 – success, а 1 — failure? Или 0 – success, а -1 — failure?)).

Существует широко распространенный алгоритм, в частности, на системах вроде Unix, когда в случае успеха функция выдает 0, а при отказе – какое-нибудь ненулевое значение (часто -1). Во многих ситуациях вариативные ненулевые результаты указывают на различные виды ошибок. Добавляя новые функции в готовые интерфейсы, важно следовать вышеупомянутому стандарту (0 эквивалентен успеху, поскольку, в целом, есть только один вариант эффективной работы функции, а вот погрешностей в ней может быть много).

Функция, созданная для анализа тех или иных условий, должна выдавать true или false. Только не путайте их с удачными/неудачными исходами запуска кода.

Функции bool обязательно присваивается имя в виде утверждения. По-английски это будет формулировка, отвечающая на вопрос да/нет. Например, is_foo() и has_widget().Функция, рассчитанная на конкретное действие, в случае с которым для вас важно знать, насколько успешно его можно выполнить, вероятно, будет задаваться другим утверждением. В некоторых языках разумно прибегать к добавлению/вычитанию исключений. На C приходится следовать определенным негласным правилам, в том числе, задавая нулевое значение для положительного результата функции.

Единственный продукт, который в 2016 году позволит форматировать продукты, разработанные на языке С, — clang-format. Родные настройки clang-format на порядок выше любого другого автоматического форматтера C-кода.

Сам я не использовал clang-format. Мне только предстоит с ним познакомиться.

Но хотелось бы озвучить несколько принципиальных моментов касательно форматирования С-кода:

  • Открытые скобки ставим в конце строки;
  • Вместо tab используем пробелы;
  • 4-колонки в одном уровне;
  • Фигурные скобки наше все (за исключением отдельных случаев, когда в целях повышения читабельности проще перечислять задачи прямо в строчку);
  • Следуйте инструкциям проекта, над которым работаете.

Я редко обращаюсь к инструментам автоматического форматирования. Может, зря?

Никогда не используйте malloc
Привыкайте к calloc.

Вот еще. Попытка обнулить все биты выделенной памяти сводится к весьма произвольному процессу, и, как правило, это не лучшая идея. Если код написан правильно, вы не сможете вызвать тот или иной объект, предварительно не присвоив ему соответствующее значение. Используя calloc, вы столкнетесь с тем, что любой баг в коде будет приравниваться к нулю, а, значит, легко будет перепутать системную ошибку с ненужными данными. Разве это похоже на усовершенствование кода?

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

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

С другой стороны, если обнуление всех битов решает поставленные задачи, можно попробовать задействовать calloc.



P.S.
Так же мы приглашаем читателей на следующей неделе посетить с экскурсией наш облачный ЦОД. Анонс мероприятия на Хабре тут.
Inoventica Services
87.24
VPS / хостинг / домены / защита от DDoS
Support the author
Share post

Comments 67

    +1
    Как обычно, обо всех ошибках и неточностях перевода прошу сообщать в ЛС или при помощи контактов, указанных в моем профиле. Так же прошу дать фидбек по верстке, особенно кода, так как в этой статье активно использовалось новая markdown-разметка. Спасибо.
      +1
      Отличная статья :) А-то от старого С 89, когда читаешь глаза вытекают :)
        +1
        Спасибо. Ссылку на оригинал этого текста дал мне один из читателей Хабра вместе с несколькими полезными замечаниями по тексту еще после публикации перевода оригинальной статьи «How To C in 2016». К сожалению, он использовал аккаунт в ВК (по всей видимости заброшенный или фейковый) и с того дня там больше не появлялся. Так что даже поблагодарить за наводку не могу. (Или смогу, если он это прочтет и опять выйдет на связь)
          +1
          Ага, штука хорошая, самому набивать такое — тяжко. Я тут недавно озаботился обратной стороной стандартов — открыл для себя классные GNU extensions — anonymous union и struct. Жалко нет хорошего каталога расширений. Конечно, использовать вещи не из стандарта — плохо, но без указанных выше фич код становится крайне некрасивым…
            +2
            Что значит «нет каталога»? Расширения языка C, Расширения языка C++

            Что вы понимаете под «anonymous union и struct»? Что-нибудь подбное?
            $ cat test1.c
            #include <stdbool.h>
            
            struct Node {
              union {
                float f;
                int i;
              };
              bool is_float;
            };
            

            Ну дык эта стандартная фича C/C++, причём тут расширения:
            $ gcc -std=c11 -pedantic -c test1.c 
            $ gcc -x c++ -std=c++98 -pedantic -c test1.c


            Я очень люблю и уважаю расширения GCC, но, тем не менее рекомендую собирать код с ключами -std=c11 или -std=c++14. Хотите расширений — укажите на это явно в кода:
            $ cat test.c
            int foo(int x) {
              __extension__ ({
                switch (x) {
                  case 0 ... 5:
                    return 1;
                  case 6 ... 8:
                    return 2;
                  default:
                    return 0;
                }
              });
            }
            $ gcc -std=c89 -pedantic -c test.c

            И вы будете понимать — где у вас стандартный код, а где расширения GNU C, и читающему будет легче.
              +1
              Для С++ — это экстеншен и -peadntic будет ругаться на это (вот так: «warning: ISO C++ prohibits anonymous structs [-Wpedantic]), для С99 — это дозволенная фича и он ругаться, соотвественно не будет :) Вообще я набил несколько шишек в попытках сделать то, что хочу без оверхеда по памяти: www.stableit.ru/2016/02/union-bit-fields-c.html
                +1
                Кто будет ругаться? На что? На стандартный код? Это ошибка компилятора, нужно исправить.

                Я оба примера прогонял через -pedantic — ровно как написано. Никаких проблем.
                  +2
                  let's code.

                  cat anonumous_struct.cpp 
                  #include <iostream>
                  
                  struct outer {
                      struct {
                          int a;
                      };
                  };
                  
                  int main() {
                      return 0;
                  }
                  


                  Стандартный компилятор из ubutnu 14.04:
                  g++ -v 2>&1|grep "gcc version"
                  gcc version 4.8.4 (Ubuntu 4.8.4-2ubuntu1~14.04) 
                  


                  g++ anonumous_struct.cpp  -pedantic 
                  anonumous_struct.cpp:6:5: warning: ISO C++ prohibits anonymous structs [-Wpedantic]
                       };
                       ^
                  


                  Самый новый gcc 5.3.0:
                  /opt/fastnetmon/libraries/gcc530/bin/g++ -v 2>&1|grep version 
                  gcc version 5.3.0 (GCC) 
                  


                  Результат:
                  /opt/fastnetmon/libraries/gcc530/bin/g++ anonumous_struct.cpp  -pedantic
                  anonumous_struct.cpp:6:5: warning: ISO C++ prohibits anonymous structs [-Wpedantic]
                       };
                       ^
                  


                  Clang:
                  clang++ --version
                  Ubuntu clang version 3.4-1ubuntu3 (tags/RELEASE_34/final) (based on LLVM 3.4)
                  Target: x86_64-pc-linux-gnu
                  Thread model: posix
                  


                  Итог:
                  clang++ anonumous_struct.cpp -pedantic
                  anonumous_struct.cpp:4:5: warning: anonymous structs are a GNU extension [-Wgnu-anonymous-struct]
                      struct {
                      ^
                  1 warning generated.
                  
                  


                  Я не верю в ошибку двух топовых С++ компиляторах одновременно. А в то, что Вы не правы — верю больше :) Если есть аргументы — прошу ссылку на стандарты, я в них плохо ориентируюсь и не могу сходу найти абзац описывающий данное поведение.
                    +3
                    Нет у меня никакой ошибки. И ошибок в компиляторе — тоже нет :-)

                    Ну кто ж вам виноват, что вы так лихо сложили в одну кучу хорошо известную и давно используемую стандартную сущность (аннонимные объединения появившиеся в C++98) и расширение GCC (анонимные структуры, описанные вот тут). Я собственно когда увидел что вы эти, сильно разные, сущности перечисляете как одну очень удивился — потому и переспросил…
                      +1
                      Я искренне предполагал, что они обе — расширение, был совершенно неправ.

                      Прогнал код с pedantic и получилось занятное:
                      anonymous structs are a GNU extension [-Wgnu-anonymous-struct]
                      anonymous types declared in an anonymous union are an extension [-Wnested-anon-types]
                      


                      То есть экстеншенов у меня аж два. Один — сам по себе анонимный struct, второй — анонимный struct внутри анонимного union (стандартного).

                      Хех. Столько фич на пустом месте :)
                        +1
                        Хех. Столько фич на пустом месте :)
                        Ну дык. В вашем исходном примере их тоже два: uint16_t fragment_offset : 13 — это тоже расширение (причём я, как бы, понимаю и разработчиков стандарта тоже: вы тут написали, с одной стороны, что бит в поле 13, с другой — что 16… так сколько же их там, чёрт побери?)…

                        Напишите лучше unsigned fragment_offset : 13 — это будет и понятно и по стандарту…
                  +2
                  Прочитал пример по ссылке. Ну да: анонимные структуры — это расширение, в стандарте есть только анонимные объединения. Ну так скажите про это компилятору и всё:
                  $ cat test.c
                  #include <stdint.h>
                  
                  typedef union __attribute__((__packed__)) {
                    __extension__ struct {
                      uint16_t fragment_offset : 13,
                      more_fragments_flag : 1,
                      dont_fragment_flag : 1,
                      reserved_flag : 1;
                    };
                    uint16_t fragmentation_details_as_integer;
                  } fragmentation_details_t;
                  $ gcc -std=c89 -pedantic -c test.c
                  $ clang -std=c89 -pedantic -c test3.c
                  Как я уже сказал: я очень люблю и уважаю расширения GCC. Но их нужно использовать аккуратно и вдумчиво. И код должен компилироваться в режиме ANSI C с -pendanticом!
                    +1
                    Ага, тут верно. Ни один компилятор не ругается на анонимные union. Но вот в моем кейсе красиво обойтись без анонимного struct не получается.

                    Но сама пометка __extension__ — разве в стандарте? Вообще, довольно удобная фича, надо весь свой код переписать под pedantic :)
                      +1
                      Тут странный финт ушами. __extension__, разумеется, в стандарте отсутствует. Но в стандарте (ещё начиная с C89 — и во всех последующих!) сказано следующее:
                      Each name that contains a double underscore (_ _) or begins with an underscore followed by an uppercase letter (2.11) is reserved to the implementation for any use
                      То есть написав два слеша подряд вы автоматически вышли за пределы действия стандарта и попали в область, описанную разработчиками GCC. Потому, кстати, __attribute__ не нужно метить специально, хотя он тоже отсутствует в стандарте.

                      Если вы посмотрите на стандартные заголовочные файлы GNU C, то увидите там кучу расширений GCC, но, разумеется, они должны со всякими -pedanticами собираться — для этого __extension__ и нужен.

                      Расширения GCC удобны (иначе зачем бы их создавали?), но, увы, малоизвестны. Потому сборка с -pedanticом и без ключа -std=gnuXX мне и кажется разумной — вы тем самым метите все места, которые нужно искать в документации на GCC, а не в документации на стандартный C/C++, что, в общем, просто удобно.
                        +3
                        Да, согласен, увидь я что-то такое в чужом коде — удивился бы довольно сильно. Explicit better than implicit :) Спасибо, хорошая штука, приведу свой код в соотвествии с этим подходом.
                          +3
                          Удачи. Обартите внимание на мой самый первый пример — с case и switch.

                          Я когда первый раз пытался обернуть его в __extension__ полдня пробился без успеха пытаясь совать его в разные места в функции. Чуть не сдался, а потом посмотрел на примеры в GNU LIBC. Оказалось: никакой магии. Просто пишите
                          __extension__({ ... }); 
                          

                          тут ({... }) — это ещё одно расширение (самое первое, как я понимаю). Позволяет засунуть внутрь выражения что угодно (описания переменных, циклы и прочее… в C (но не в C++) — можно дажи функций там наописывать и там же их вызвать!). А уже внутри там — можно и другие расширения использовать.
                            +1
                            А может осилите отдельную крутую статью по расширениям? :) Дабы такая чудная магия не прозябала! Ведь задача тут одна — описать как можно лучше то, что в машинном коде реализуется нэтивно. И когда смотришь решения — тот же мультикейс напрашивается сам собой…
                              +5
                              Ну… крутую статью не обещаю, но немного как-то систематизировать использование расширений… попробую.
                                +1
                                Буду бесконечно благодарен :)
                                  0
                                  Как и обещал включил -pedantic и разметил везде, где используются расширения этот факт явно:

                                  -                struct __attribute__((__packed__))  {
                                  +                __extension__ struct __attribute__((__packed__))  {
                                    0
                                    Ну тут я даже не знаю что посоветовать: __attribute__ как бы уже подразумевает __extension__ (так как два подчёркивания — см. выше), с другой — как некоторая дополнительная документирующая пометка — тоже имеет право на существование. А вот -pedantic — это хорошо однозначно.

                                    P.S. По-хорошему им бы нужно сделать градации типа -Wgcc5, -Wgcc6 и т.д.: тогда можно было бы врубать сразу самые сильные warning'и и -Werror не боясь того, что "со всем этим барахлом" оно перестанет собираться на будущих версиях gcc… Мечты, мечты...
                                  +1
                                  А мне как делающему первые шаги в С хотелось бы тоже почитать =)
              +1
              Старому си не хватает что-нибудь вроде goшного defer, чтобы было что-то вроде:
              FILE *fp = fopen(...);
              if (fp == NULL) {
              // обработка ошибки
              } else {
              defer fclose(fp);
              }
              Чтобы не приходилось отслеживать закрытие файла, к примеру.
                +2
                «Старый Си» исповедует принцип WYSIWYG (что видите — то и получите): никакой «скрытой» магии. Глядя на любую его конструкцию можно без труда понять что где происходит. Грубо говоря N байт на входе порождают не более 100N на выходе (после препроцессирования, разумеется). Можно на это смотреть как на «баг» или как на «фичу», но ломать этот подход — не стоит. Это будет уже совсем другой язык.

                Правда в GCC уже есть одна фича, которая может приводить к «геометрическому» разбуханию кода — но она редко используется и на практике неопасна. defer — в этом смысле куда потенциально опаснее…
                  0
                  Так вроде же сокращается количество кода (например точка выхода с EXIT_FAILURE, тут бы память почистить, да обработать выше, дублируем чистку памяти из конца программы, нужен defer, ибо даже finally такой гибкости не даст)?
                  Дай ка пример, при котором количество кода увеличится?
                    +2
                    Вопрос не в "увеличении количества кода". Вопрос в "нелокальности". Глядя на какую-то конкретную точку в программе на C вы всегда можете понять — что тут происходит. Смотрите на if (a==b) { — видите: ага, из памяти читаем a, b, сравниваем… пять инструкций, одна и которых — goto. Видим "}" — тут, в общем, возможны варианты, но это либо jmp, либо ничего (в зависимости от того — это конец цикла или просто блока).

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

                    Всякие finally, defer, деструкторы и прочее эту локальность разрушают. Что у вас после этого порождает return или просто закрывающая фигурная скобка — одному богу ведомо! Нужно целое расследование проводить, чтобы это понять! Причём разные returnы в одной и той же функции — ведут себя по разному.

                    Это не хорошо и не плохо. Это просто по-другому. Сильно по другому — совсем не так, как в C!

                    Я понимаю, что вам хочется, чтобы C вёл себя "поудобнее". Но это вам станет удобнее! А всем тем, кто с С работает многие годы (а то и десятилетия) — станет неудобно!

                    Да, можно много чего добавить в С. И это много раз проделывалось. Получались всякие C++, Objective-C и так далее. Но всё это — уже не C! Это — другие языки! С совсем другими свойствами!

                    Я исренне рад тому, что разработчики C99, C11 (и, надеюсь, C22 и так далее) удержались от соблазна добавить всяких defer...
                  +1
                  Это возможно даже в С fdiv.net/2015/10/08/emulating-defer-c-clang-or-gccblocks =)
                –2
                Если подразумеваются байты, задействуйте unsigned char. Если речь об октетах, выбирайте uint8_t.

                Разве в байте не всегда 8 бит? В char может быть сколько угодно бит, но разве char — это синоним байта (а не символа)?
                  +5
                  Все примитивные типы в Си являются платформозависимыми и могут иметь любой размер. И да, в байте может быть не 8 бит.
                    +1
                    Про типы в С я в курсе. Но я никогда не видел и не слышал, чтобы где-то всерьез говорилось о байте не из 8 бит.
                    Да, в 60 или 70 были машины со странной длиной слова или 5-6-7 битами для представления символов. Но мне казалось, что слово байт является синонимом для слова октет.

                    Я в курсе, что и сейчас есть машины, где CHAR_BITS не равно 8, но ведь это размер char, а не размер байта… или нет? Я запутался.
                      +6
                      Мой любимый пример — DSP. Можно найти архитектуру, где sizeof(int) == sizeof(short) == sizeof(char) == 1, при этом char имеет размер 32 бита. Просто потому что процессор не может работать с типами другой длины.

                      Октет — это ровно 8 бит.
                      Байт — это минимально адресуемая ячейка памяти. Да, в x86 они совпадают, но это не значит что они должны совпадать в других архитектурах.
                        +2
                        Cray также был устроен. То же самое: sizeof(int) == sizeof(short) == sizeof(char) == 1, CHAR_BITS == 32.
                          +1
                          Байт — это минимально адресуемая ячейка памяти.

                          А вы уверены, что это общепринятое определение?
                            +3
                            Это то, что написано в стандартах C и C++. Так что «по умочанию» при обсуждении C и C++ должно использоваться именно оно.
                              +1
                              Да, вы правы. Мне казалось, что стандарт С вообще не оперирует понятием байт.
                                +1
                                К сожалению или к счастью — но оперирует. И байт — это именно размер переменной типа char.

                                Вы, в какой-то степени, правы: очень мало людей знаю как именно стандарт определяет байт. Большинство либо думают, что стандарт этим понятием не оперирует, либо думают что речь идёт про 8-битовый байт.

                                Так что определение действительно не вполне «общепринятое». Но, с другой стороны, если не опираться при обсуждении C/C++ на стандарт, то… на что тогда вообще опираться?
                  • UFO just landed and posted this here
                      +1
                      Многие вещи нельзя понять, когда только начинаешь писать на С. Ну не пишут в книгах, а примеры кода на С — обычно поганейшие, потому что обычно там С 89 со всеми вытекающими. А Вот 99й стандарт просто прелесть :)
                      • UFO just landed and posted this here
                          +2
                          Ну кому нужен сахар — пишут на С++, при прочих равных и более высоком удобстве он генерирует ровно такой же по эффективности машинный код. Лично я предпочитаю С++.
                          • UFO just landed and posted this here
                              +2
                              Скажу Вам как С/С++-шник — прелесть C++ — в его возможности использовать только то, что нужно, вплоть до подмножества С, тогда код будет абсолютно идентичным. Страуструп не раз говорил, что этот принцип будет сохраняться — платить только за то, что используешь, неиспользуемые фичи никаким образом не влияют на код. Лучше использовать с умом С++, особенно учитывая, что многие фичи даются абсолютно бесплатно — классы без виртуальных методов и с отключенными rtti идентичны структурам; constexpr, namespaces, перегрузка функций, операторов, значения по умолчанию для аргументов и пр.
                              • UFO just landed and posted this here
                                  +2
                                  > функциональщины

                                  кхм
                                  • UFO just landed and posted this here
                                      +2
                                      Да просто к термину придрался. «Функциональщиной» все-таки обычно называют вычисления без побочных эффектов, а вышеописанное — это «процедурщина» :).
                                      • UFO just landed and posted this here
                                –1
                                Скажу вам как сишник С++ не умеет генерировать машинный код так же эффективно как С.

                                Да, так же не умеет, зачастую получается быстрее.

                                Да, бинарники получаются больше, но, по крайней мере, в моих задачах скорость работы на порядки важнее непосредственно размера бинарника.
                                  0
                                  К огромному сожалению на C++ (да ещё и с шаблонами!) очень часто пишут даже те вещи, для которых важен размер. Такие как стандартная библиотека или ядро.

                                  Почему для «вспомогательных» вещей лучше использовать C (или, по крайней мере, не использовать исключения и шаблоны)? Потому что кеш процессора не резиновый. Чем больше его себе отгрызёт какой-нибудь printf, тем меньше останется на основную задачу. Некоторые функции стандартной библиотеки, наоборот, используются где-нибудь во внутренних циклах (memcmp или memset), но тут, скорее, нужно всё аккуратно на ассемблере написать, чем шаблоны использовать…
                                    –1
                                    К огромному сожалению на C++ (да ещё и с шаблонами!) очень часто пишут даже те вещи, для которых важен размер. Такие как стандартная библиотека или ядро.

                                    Для встраиваемых систем? Согласен, там размер важен. Хотя у меня опыт написания на C++ под attiny весьма положительный. Да ещё и с шаблонами!
                                    Важность размера для обычных настольных машин и серверов не столь очевидна.

                                    Потому что кеш процессора не резиновый.

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

                                    Чем больше его себе отгрызёт какой-нибудь printf, тем меньше останется на основную задачу.

                                    Если у вас в основном цикле выполняется printf, то у вас и так IO сожрёт больше, чем RAM access.

                                    Впрочем, не IO единым, поделюсь одной историей из личной практики. Была софтина, которой нужно было прочитать текстовый файл размером в несколько гигабайт. Так как софтину в процессе разработки запускали часто, то файл обычно был в кеше ФС в памяти, и это уже становилось узким местом само по себе. И захотелось мне это дело немножко ускорить — всё-таки тратить под сто секунд на чтение и парсинг (и формирование неких структур данных, но это уже не суть) этого файла при каждом тестовом прогоне — не дело.

                                    И вот как-то так получилось, что даже тупые iostreams оказались быстрее fscanf("%s %s %d"), потому что fscanf выполняет разбор строки формата при каждом вызове, а у fstream формат разбора «закодирован» непосредственно в цепоче вызовов.

                                    У fstream'а я, впрочем, не смог победить 10% времени внутри какой-то наркомании с локалями, и в итоге оптимальным оказалось вообще использовать mmap() + boost::string_ref, замена которым обычных std::string, кстати, выполнилась легко, просто и безопасно, спасибо шаблонам.

                                    Некоторые функции стандартной библиотеки, наоборот, используются где-нибудь во внутренних циклах (memcmp или memset)

                                    Ну и отлично. Компилятор у вас увидит в компил-тайме, что вы вызываете memcmp на типе длиной в 8 байт, и вместо гроздьев проверок в рантайме выберет подходящую специализацию вот прям здесь. И всё это портабельно и переносимо.

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

                                    но тут, скорее, нужно всё аккуратно на ассемблере написать, чем шаблоны использовать…

                                    Замучаетесь с ассемблером бегать и при выходе новой микроархитектуры или расширенного набора инструкций свой ассемблер переписывать. Давайте лучше это авторам компиляторов оставим?
                                      +2
                                      Для встраиваемых систем?
                                      Для небольших таких встраиваемых систем. С 64GiB памяти и 48 ядрами на борту.

                                      Важность размера для обычных настольных машин и серверов не столь очевидна.
                                      Ну как вам сказать. Есть ведь простой способ проверить: влючаем -Os — и получаем «заметную прибавку к пенсии». А разница между -O2 и -Os — это мелочь по сравнению с тем, что шаблоны могут сделать.

                                      Хотя у меня опыт написания на C++ под attiny весьма положительный. Да ещё и с шаблонами!
                                      А вот тут как раз проблем гораздо меньше: «движущихся частей» не так много, эффекты все заметить гораздо проще.

                                      Весьма редко нужно запихивать в кеш процессора весь бинарник, обычно нужно запихнуть в кеш основной считающий цикл.
                                      Зависит от задачи. Если у вас чисто счётная задача и там есть несколько очень горячих циклов — тогда может быть. А если у вас сервер с сотнями мегабайт кода? Из них вполне может быть «горячего кода» и мегабайт и два и три. Где вы столько кеша найдётё? Даже самый наираспоследний Хасвелл имеет 32KiB кеша первого уровня и 256KiB — второго. И это число нифига не растёт! Кеш нужно расходовать не бережно, а очень бережно.

                                      Это памяти (это та, которая «new disk») у вас может быть много. Кеша же много не бывает!

                                      Если у вас в основном цикле выполняется printf, то у вас и так IO сожрёт больше, чем RAM access.
                                      Это с какого-такого перепугу? printf'у не обязательно вызываться на каждом «обороте» цикла. Он может сбрасывать «отчёт» раз в 1000 повторов (или раз в миллион). Но ваш код он будет из кеша успешно выкидывать в любом случае.

                                      Замучаетесь с ассемблером бегать и при выходе новой микроархитектуры или расширенного набора инструкций свой ассемблер переписывать. Давайте лучше это авторам компиляторов оставим?
                                      Некоторые — уже пробовали. Результат — мы знаем.

                                      Компилятор — весьма тупая скотина. Скопировать 8 байт с помощью mov'а — он ещё может придумать. А вот использовать SSE4.2 — уже никак. Вернее он SSE4.2 научится использовать тогда, когда все будут уже об AVX1024 каком-нибудь говорить. Посмотрите на этот patch, посмотрите куда он применяется и подумайте — почему (обратите внимание на обратный адрес автора, кстати).

                                      У fstream'а я, впрочем, не смог победить 10% времени внутри какой-то наркомании с локалями, и в итоге оптимальным оказалось вообще использовать mmap() + boost::string_ref, замена которым обычных std::string, кстати, выполнилась легко, просто и безопасно, спасибо шаблонам.
                                      Ну дык. Вначале вы проигрываете кучу времени на шаблонах, потом с профайлером можете отыграть сколько-то назад. Но это вовсе не значит, что вы выиграете у C.

                                      Из моего личного опыта: был у нас компонент, который являлся bottleneck'ом для некоего workload'а. И его разработчики тоже много раз оптимизировали — и это работало: грамотное применения профайлера, рефакторинги (которые были возможны из-за «правильной архитектуры») и прочее. Ускорили в конечном итоге почти в 4 раза. А потом пришёл я. С версией на C, которая была в 12 раз быстрее оригинальной :-) Причём я, в отличие от них, не пользовался ни профайлерами, ни «новёйшими технологиями». Правда там кодогенератор был использован — ну так кодогенераторы ещё в 60е появились.

                                      Конечно есть и обратные примеры: Gold, к примеру, работает гораздо быстрее ld — ну так это за счёт того, что его-то писал человек, знающий не только C++. Он написан для того, чтобы быть быстрым изначально, а не для того, чтобы его можно было ускорить погоняв профилировщик :-)

                                      То есть тот феномен, что программы на C++, как правило, медленнее, чем программы на C — это в основном психологический феномен, не технический. Но… он таки имеет место быть.
                                        0
                                        Для небольших таких встраиваемых систем. С 64GiB памяти и 48 ядрами на борту.

                                        А таки зачем там?

                                        Зависит от задачи. Если у вас чисто счётная задача и там есть несколько очень горячих циклов — тогда может быть. А если у вас сервер с сотнями мегабайт кода? Из них вполне может быть «горячего кода» и мегабайт и два и три.

                                        Тогда нужно в любом случае привязывать задачи к отдельным ядрам. Задача свелась к предыдущей.

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

                                        Это с какого-такого перепугу? printf'у не обязательно вызываться на каждом «обороте» цикла. Он может сбрасывать «отчёт» раз в 1000 повторов (или раз в миллион). Но ваш код он будет из кеша успешно выкидывать в любом случае.

                                        Так, стоп, а сишный код не будет выкидывать, что ли?

                                        Компилятор — весьма тупая скотина.

                                        Тут, к сожалению, чаще соглашусь. Хотя я весьма часто был достаточно удовлетворён результатами его работы, чтобы дополнительный условный процент скорости не окупился парой дней моей работы сейчас и N днями работы команды по поддержке этого всего счастья потом.

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

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

                                        Потом, кстати, я разделители пробовал искать что через memchr, что через std::find_if, и второй подход оказался чуточку быстрее — ЕМНИП было меньше всякой подготовки перед тем, как всякую векторную наркоманию по массиву из char'ов гонять. Компилятор соптимизировать больше может.

                                        А потом пришёл я. С версией на C, которая была в 12 раз быстрее оригинальной :-)

                                        А из-за чего был такой выигрыш, если не секрет?

                                        То есть тот феномен, что программы на C++, как правило, медленнее, чем программы на C — это в основном психологический феномен, не технический. Но… он таки имеет место быть.

                                        Не только психологический.

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

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

                                          А случаи, когда незаинлайненный вызов внутри горячего цикла выгоднее, мне бы ещё посмотреть.
                                          Сморите, ищите. Пример был дан выше. Что-что? Большой, сложный, неудобный? А кому сейчас легко: да, на игрушечных примерах всё работает так, как вы говорите. На больших, реальных — нет. Добро пожаловать в реальный мир!

                                          Так, стоп, а сишный код не будет выкидывать, что ли?
                                          Сишного кода будет меньше. И его влияние будет меньше. Мир — он не чёрно-белый. Тут и другие оттенки есть.

                                          А из-за чего был такой выигрыш, если не секрет?
                                          За счёт того, что, увы и ах, но шаблонная магия не исчезает из кода бесследно. Даже современные компиляторы не могут переработать структуры данных, они только с кодом работают. Там где версия на C++ имела всевозможные параметризованные типы в C'шной версии был простой int32_t с грамотно выверенными битовыми полями. Причём пересекающимися (то есть «есть тут стоит такой бит, то смотреть там, если не стоит — смотреть тут»). И потому попросту данных C'шная версия гоняла по шине гораздо меньше. Вот и всё. Ну ещё она не имела кучи шаблонных уровней индирекции, но я не уверен, что это сильно сработало.
                                  • UFO just landed and posted this here
                                      –2
                                      О, а вот и любители C без аргументов пришли.

                                      Начнём с того, что компилятору C++ гораздо проще дать больше информации о типах, используемых в конкретной инстанциации какой-нибудь шаблонной функции: это всё у него прям под рукой.
                                        0
                                        Начнём с того, что компилятору C++ гораздо проще дать больше информации о типах, используемых в конкретной инстанциации какой-нибудь шаблонной функции: это всё у него прям под рукой.
                                        А стоит ли ему их тут давать? Мой опыт показывают, что разработчики на C++ очень часто переоцинивают выигрыш от подобных вещей и недооценивают возможные последствия.

                                        Мой опыт показывает, что написать на C++ небольшой кусочек, который на всех бенчмарках порвёт код на C — легко. А вот всю программу… или хотя бы достаточно большую систему… тут всё гораздо сложнее.
                                          0
                                          А стоит ли ему их тут давать? Мой опыт показывают, что разработчики на C++ очень часто переоцинивают выигрыш от подобных вещей и недооценивают возможные последствия.

                                          А какие последствия?

                                          Мой опыт показывает, что написать на C++ небольшой кусочек, который на всех бенчмарках порвёт код на C — легко.

                                          Всякие числодробилки вполне можно рассматривать как такой небольшой кусочек плюс всю сопутствующую обвязку. Или композицию таких кусочков. Тем более, что…

                                          А вот всю программу… или хотя бы достаточно большую систему… тут всё гораздо сложнее.

                                          …нет, на плюсах вообще большие системы описывать проще.
                                            +1
                                            А какие последствия?
                                            «Рыхлые» структуры данных, «рыхлый» код — и невозможность что-либо с этим сделать на поздних стадиях. Причём проявляется это, зачастую, как раз на поздних стадиях: пока у вас всё мальнькое и помещается в кеш, всё работает, но когда у вас программа становится большой… в своё время Линус сказал: Trust me: every problem in computer science may be solved by an indirection, but those indirections are expensive. Pointer chasing is just about the most expensive thing you can do on modern CPU's.

                                            Разработчики C++ в стремлении организовать свои структуры удобным для обработки через шаблоны образом часто об этом забывают.

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

                                            В этом вся проблема. Да, C++ даёт инструменты, которые при очень аккуратном применении могут дать результат лучше, чем Go или C. Но, к сожалению, для этого нужны очень грамотные люди. А их мало. При написании большой системы велик шанс, что общий итог будет скорее замедление по сравнению с тем, что было бы на C, чем выигрыш.

                                            Практически эту тенденцию зачастую пытаются обуздать путём создания разного рода Style Guide'ов, которые просто директивно запрещают некоторые возможности C++ использовать, но в результате получается что и те, кто мог бы их использовать эффективно этого делать после этого не могут.

                                            В общем сложно всё с C++, очень сложно. Посмотрим как rust справится или может ещё что-нибудь. Пока неясно — можно ли на всём этом вообще писать большие системы и насколько это эффективно, но первые результаты обнадёживают…
                                        0
                                        Если вы про то, что код, который генерирует G++ хуже, чем то, что генерирует GCC — то ваши сведения устарели. На одинаковых программах разницы нет. На это ушло много лет, да, но это всё-таки случилось. И довольно давно. Где-то к концу нулевых :-)

                                        Так что «самая быстрая» программа на C и «самая быстрая» программа на C++ сегодня будут иметь одинаковую скорость.

                                        А вот «типичная»… тут всё гораздо, гораздо сложнее…
                                  +4
                                  memcmp() это ещё более-менее. А вот strstr()…
                                    +1
                                    Эти функции имеют короткие имена из-за трансляторов тех времён (когда эти функции создавались), которые больше 6 символов не понимали и не различали регистр.
                                    +1
                                    пускай почитает ядро линукса

                                    Ядро линукса, как и большинство программ GNU, написаны на достандартовом C (диалект K&R). Почитай новые программы типа Git'а, там увидишь нормальный, современный C.
                                    +2
                                    Ну не пишут в книгах, а примеры кода на С — обычно поганейшие, потому что обычно там С 89 со всеми вытекающими. А Вот 99й стандарт просто прелесть

                                    Это ты путаешь диалект K&R со стандартом C89. C89 и C99 практически ничем не отличаются по своему внешнему виду.
                                    Первое издание книги «Язык программирования C» содержал примеры на том достандартовом диалекте, но есть второе издание книги, где примеры на ANSI С89.
                                  +5
                                  Вы меня, конечно, извините, но это чушь. В частности, int – самый приемлемый тип целочисленных данных для текущей платформы. Если речь идет о быстрых беззнаковых целых, как минимум, на 16 битов, нет ничего плохого в использовании int (или можно ссылаться на опцию int_least16_t, которая прекрасно справится с функциями того же типа, но ИМХО это куда подробнее, чем оно того стоит).


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

                                  Например, есть такой очень классный плеер, deadbeef. Он написан на С и вполне себе кроссплатформенный. Однако зачем-то его авторы решили придумать свой формат плейлистов, причём не текстовый, а бинарный. И в этом формате используются платформозависимые типы данных, с платформозависимым порядком байт. Писать программу не на C (конкретно — на Rust) для разбора плейлистов из-за этого было больно. Кроме того, я более чем уверен, что если deadbeef запустить, скажем на ARM, и скормить ему плейлист, сделанный им же, но на Linux x86_64, то он его не проглотит вообще.

                                  Далее, есть такая библиотека, ejdb. В её API применяются платформозависимые типы данных. Из-за этого при написании байндингов в нормальном языке (где размеры типов фиксированы) нужно всегда следить за тем, чтобы использовать типы подходящего размера. И то, за этим не всегда можно уследить, и я вполне могу себе представить, что на другой платформе байндинг может не заработать правильно, или отвалиться на слишком больших значениях.

                                  В общем, если программа на C пишется исключительно для себя и на один раз/для одного конкретного применения, то, вероятно, платформозависимые типы использовать можно. Но если программа должна быть хоть сколько-то кроссплатформенной, то использование платформозависимых типов эквивалентно созданию минного поля из граблей с привязанными топорами. А уж если это библиотека, которая предполагает использование из других программ, то неиспользование фиксированных типов это просто неуважение к её пользователям.
                                    +1
                                    Всё зависит от назначения кода. Если вы описываете структуру данных для обмена, как в вашем примере, то целесообразно использовать платформонезависимые типы, так как это обеспечивает совместимость. Если вы описываете какие-то абстрактные внутренние переменные, как, например, счётчик цикла от 1 до 10, то целесообразно использовать платформозависимые типы, так как это обеспечивает эффективность. Хороший кроссплатформенный код требует именно чёткого понимания, в каком случае требуется использовать платформонезависимый тип, а в каком – платформозависимый.
                                    0
                                    Но ведь этот вариант ничуть не лучше.

                                    Если хотите подчеркнуть разницу типов, пишите:

                                    ptrdiff_t diff = ptrOld — ptrNew;

                                    Так ведь в оригинале считалась разница в адресе (что, собственно, и подразумевается при использовании ptrdiff_t), а у вас — количество элементов между указателями, зависящее от типа указателя (а для этого надо использовать тип size_t).
                                      +1
                                      >> Правильный тип для указателей в данном случае — uintptr_t, он задается файлами <stdint.h>.

                                      Какая жуткая ошибка.

                                      Начнем с мелких погрешностей: uintptr_t задается <stdint.h>, а не <stddef.h>.

                                      Вообще-то и в цитате, и в оригинале именно так и написано: «The correct type for pointer math is uintptr_t defined by <stdint.h>, while the also useful ptrdiff_t is defined by stddef.h.». Кто ошибся?

                                      Only users with full accounts can post comments. Log in, please.