Наша программа обрабатывает сетевые пакеты, в частности, заголовки TCP/IP/etc. В них числовые значения — смещения, счетчики, адреса — представлены в сетевом порядке байтов (big-endian); мы же работаем на x86 (little-endian). В стандартных структурах, описывающих заголовки, эти поля представлены простыми целочисленными типами (uint32_t, uint16_t). После нескольких багов из-за того, что порядок байтов забыли преобразовать, мы решили заменить типы полей на классы, запрещающие неявные преобразования и нетипичные операции. Под катом — утилитарный код и конкретные примеры ошибок, которые выявила строгая типизация.

Порядок байтов


Ликбез для тех, кто не в курсе про порядок байтов (endianness, byte order). Более подробно уже было на «Хабре».

При обычной записи чисел слева идут от старшего (слева) к младшему (справа): 43210 = 4?102 + 3?101 + 2?100. Целочисленные типы данных имеют фиксированный размер, например, 16 бит (числа от 0 до 65535). В памяти они хранятся как два байта, например, 43210 = 01b016, то есть байты 01 и b0.

Напечатаем байты этого числа:

#include <cstdio>   // printf()
#include <cstdint>  // uint8_t, uint16_t

int main() {
    uint16_t value = 0x01b0;
    printf("%04x\n", value);

    const auto bytes = reinterpret_cast<const uint8_t*>(&value);
    for (auto i = 0; i < sizeof(value); i++) {
        printf("%02x ", bytes[i]);
    }
}

На обычных процессорах Intel или AMD (x86) получим следующее:

01b0
b0 01

Байты в памяти расположены от младшего к старшему, а не как при записи чисел. Такой порядок называется little-endian (LE). То же верно для 4-байтовых чисел. Порядок байтов определяется архитектурой процессора. «Родной» для процессора порядок называется еще порядком ЦП или хоста (CPU/host byte order). В нашем случае host byte order — это little-endian.

Однако интернет рождался не на x86, и там порядок байтов был другой — от старшего к младшему (big-endian, BE). Его и стали использовать в заголовках сетевых протоколов (IP, TCP, UDP), поэтому big-endian еще называют сетевым порядком байтов (network byte order).

Пример: порт 443 (1bb16), по которому ходит HTTPS, в заголовках TCP записан байтами bb 01, которые при чтении дадут bb0116 = 47873.

// Все uint16_t и uint32_t здесь в сетевом порядке байтов.
struct tcp_hdr {
    uint16_t th_sport;
    uint16_t th_dport;
    uint32_t th_seq;
    uint32_t th_ack;
    uint32_t th_flags2 : 4;
    uint32_t th_off    : 4;
    uint8_t  th_flags;
    uint16_t th_win;
    uint16_t th_sum;
    uint16_t th_urp;
} __attribute__((__packed__));

tcp_hdr* tcp = ...; // указатель на часть сетевого пакета

// Неправильно: dst_port в BE, а 443 в LE.
if (tcp->dst_port == 443) { ... }

// Неправильно: ++ оперирует LE, а sent_seq в BE.
tcp->sent_seq++;

Порядок байтов в числе можно преобразовывать. Например, для uint16_t есть стандартная функция htons() (host to network for short integer — из порядка хоста в сетевой порядок для коротких целых) и обратная ей ntohs(). Аналогично для uint32_t есть htonl() и ntohl() (long — длинное целое).

// Правильно: сравниваем BE поле заголовка с BE значением.
if (tcp->dst_port == htons(443)) { ... }

// Сначала переводим BE значение из заголовка в LE, увеличиваем на 1,
// затем переводим LE сумму обратно в BE.
tcp->sent_seq = htonl(ntohl(tcp->sent_seq) + 1);

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

Строгая типизация


Риск перепутать порядок байтов очевиден, как с ним бороться?

  • Code review. В нашем проекте это обязательная процедура. К сожалению, проверяющим меньше всего хочется вникать в код, который манипулирует байтами: «вижу htons() — наверное, автор обо всем подумал».
  • Дисциплина, правила наподобие: BE только в пакетах, все переменные в LE. Не всегда разумно, например, если нужно проверять порты по хэш-таблице, эффективнее хранить их в сетевом порядке байтов и искать «как есть».
  • Тесты. Как известно, они не гарантируют отсутствие ошибок. Данные могут быть неудачно подобраны (1.1.1.1 не меняется при преобразовании порядка байтов) или подогнаны под результат.

При работе с сетью нельзя абстрагироваться от порядка байтов, поэтому хотелось бы сделать так, чтобы его нельзя было проигнорировать при написании кода. Более того, у нас не просто число в BE — это номер порта, IP-адрес, номер последовательности TCP, контрольная сумма. Одно нельзя присваивать другому, даже если количество бит совпадает.

Решение известно — строгая типизация, то есть отдельные типы для портов, адресов, номеров. Кроме того, эти типы должны поддерживать конвертацию BE/LE. Boost.Endian нам не подходит, так как в проекте нет Boost.

Размер проекта около 40 тысяч строк на C++17. Если создать безопасные типы-обертки и переписать на них структуры заголовков, автоматически перестанут компилироваться все места, где есть работа с BE. Придется один раз пройтись по ним всем, зато новый код будет только безопасным.

Класс числа в big-endian
#include <cstdint>
#include <iosfwd>

#define PACKED __attribute__((packed))

constexpr auto bswap(uint16_t value) noexcept {
    return __builtin_bswap16(value);
}

constexpr auto bswap(uint32_t value) noexcept {
    return __builtin_bswap32(value);
}

template<typename T>
struct Raw {
    T value;
};

template<typename T>
Raw(T) -> Raw<T>;

template<typename T>
struct BigEndian {
    using Underlying = T;
    using Native = T;

    constexpr BigEndian() noexcept = default;
    constexpr explicit BigEndian(Native value) noexcept : _value{bswap(value)} {}
    constexpr BigEndian(Raw<Underlying> raw) noexcept : _value{raw.value} {}

    constexpr Underlying raw() const { return _value; }
    constexpr Native native() const { return bswap(_value); }

    explicit operator bool() const {
        return static_cast<bool>(_value);
    }

    bool operator==(const BigEndian& other) const {
        return raw() == other.raw();
    }

    bool operator!=(const BigEndian& other) const {
        return raw() != other.raw();
    }

    friend std::ostream&
    operator<<(std::ostream& out, const BigEndian& value) {
        return out << value.native();
    }

private:
    Underlying _value{};
} PACKED;


  • Заголовочный файл с этим типом будет включаться повсеместно, поэтому вместо тяжелого <iostream> используется легковесный <iosfwd>.
  • Вместо htons() и т. п. — быстрые интринсики компилятора. В частности, на них действует constant propagation, поэтому конструкторы constexpr.
  • Иногда уже есть значение uint16_t/uint32_t, находящееся в BE. Структура Raw<T> с deduction guide позволяет удобно создать из него BigEndian<T>.

Спорным моментом здесь является PACKED: считается, что упакованные структуры хуже поддаются оптимизации. Ответ один — мерить. Наши бенчмарки кода не выявили замедления. Кроме того, в случае сетевых пакетов положение полей в заголовке все равно фиксировано.

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

using BE16 = BigEndian<uint16_t>;
using BE32 = BigEndian<uint32_t>;

struct Seqnum : BE32 {
    using BE32::BE32;

    template<typename Integral>
    Seqnum operator+(Integral increment) const {
        static_assert(std::is_integral_v<Integral>);
        return Seqnum{static_cast<uint32_t>(native() + increment)};
    }
} PACKED;

struct IP : BE32 {
    using BE32::BE32;
} PACKED;

struct L4Port : BE16 {
    using BE16::BE16;
} PACKED;

Безопасная структура заголовка TCP
enum TCPFlag : uint8_t {
    TH_FIN = 0x01,
    TH_SYN = 0x02,
    TH_RST = 0x04,
    TH_PUSH = 0x08,
    TH_ACK = 0x10,
    TH_URG = 0x20,
    TH_ECE = 0x40,
    TH_CWR = 0x80,
};

using TCPFlags = std::underlying_type_t<TCPFlag>;

struct TCPHeader {
    L4Port   th_sport;
    L4Port   th_dport;
    Seqnum   th_seq;
    Seqnum   th_ack;
    uint32_t th_flags2 : 4;
    uint32_t th_off    : 4;
    TCPFlags th_flags;
    BE16     th_win;
    uint16_t th_sum;
    BE16     th_urp;

    uint16_t header_length() const {
        return th_off << 2;
    }

    void set_header_length(uint16_t len) {
        th_off = len >> 2;
    }

    uint8_t* payload() {
        return reinterpret_cast<uint8_t*>(this) + header_length();
    }

    const uint8_t* payload() const {
        return reinterpret_cast<const uint8_t*>(this) + header_length();
    }
};

static_assert(sizeof(TCPHeader) == 20);

  • TCPFlag можно было бы сделать enum class, но на практике над флагами делается всего две операции: проверка вхождения (&) либо замена флагов на комбинацию (|) — путаницы не возникает.
  • Битовые поля оставлены примитивными, но сделаны безопасные методы доступа.
  • Названия полей оставлены классическими.

Результаты


Большинство правок были тривиальными. Код стал чище:

     auto tcp = packet->tcp_header();
-    return make_response(packet,
-            cookie_make(packet, rte_be_to_cpu_32(tcp->th_seq)),
-            rte_cpu_to_be_32(rte_be_to_cpu_32(tcp->th_seq) + 1),
-            TH_SYN | TH_ACK);
+    return make_response(packet, cookie_make(packet, tcp->th_seq.native()),
+            tcp->th_seq + 1, TH_SYN | TH_ACK);
 }

Отчасти типы документировали код:

-    void check_packet(int64_t, int64_t, uint8_t, bool);
+    void check_packet(std::optional<Seqnum>, std::optional<Seqnum>, TCPFlags, bool);

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

     // меняем window size
     auto wscale_ratio = options().wscale_dst - options().wscale_src;
     if (wscale_ratio < 0) {
-        auto window_size = header.window_size() / (1 << (-wscale_ratio));
+        auto window_size = header.window_size().native() / (1 << (-wscale_ratio));
         if (header.window_size() && window_size < 1) {
             window_size = WINDOW_SIZE_MIN;
         }
         header_out.window_size(window_size);
     } else {
-        auto window_size = header.window_size() * (1 << (wscale_ratio));
+        auto window_size = header.window_size().native() * (1 << (wscale_ratio));
         if (window_size > WINDOW_SIZE_MAX) {
             window_size = WINDOW_SIZE_MAX;
         }

Пример логической ошибки: разработчик оригинального кода думал, что функция принимает BE, хотя на самом деле это не так. При попытке использовать Raw{} вместо 0 программа просто не компилировалась (к счастью, это лишь unit-тест). Тут же видим неудачный выбор данных: ошибка нашлась бы скорее, если бы использовался не 0, который одинаков в любом порядке байтов.

-    auto cookie = cookie_make_inner(tuple, rte_be_to_cpu_32(0));
+    auto cookie = cookie_make_inner(tuple, 0);

Аналогичный пример: сначала компилятор указал на несоответствие типов def_seq и cookie, затем стало ясно, почему тест проходил раньше — такие константы.

-    const uint32_t def_seq = 0xA7A7A7A7;
-    const uint32_t def_ack = 0xA8A8A8A8;
+    const Seqnum def_seq{0x12345678};
+    const Seqnum def_ack{0x90abcdef}; ...
-    auto cookie = rte_be_to_cpu_32(_tcph->th_ack);
+    auto cookie = _tcph->th_ack; ASSERT_NE(def_seq, cookie);

Итоги


В сухом остатке имеем:

  • Найден один баг и несколько логических ошибок в unit-тестах.
  • Рефакторинг заставил разобраться в сомнительных местах, читаемость возросла.
  • Производительность сохранилась, но могла бы снизиться — бенчмарки нужны.

Нам важны все три пункта, поэтому считаем, рефакторинг того стоил.

А вы страхуете себя от ошибок строгими типами?

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


  1. ilammy
    13.10.2019 01:03
    +3

    А вы страхуете себя от ошибок строгими типами?

    Запретом на reinterpret_cast.


    Вот весь пост — прямо классическая иллюстрация причины, скрывающейся за правилом: никогда не парсить бинарные данные с помощью приведения к типу указателя на __attribute__((packed)) структурки.


    Кроме возможных проблем с порядком байтов, которые рассматриваются здесь, подобные махинации ещё могут нарушать требования к выравниванию типов. Естественно, всё это работает на x86 и современных ARM, потому что производители железа сдались. Но формально нарушение выравнивания является undefined behavior, и существуют архитектуры, где это фатально.


    1. dmxvlx
      13.10.2019 01:16
      +1

      никогда не парсить бинарные данные с помощью приведения к типу указателя на attribute((packed)) структурки.

      полностью согласен. без memcpy никуда...


    1. esaulenka
      13.10.2019 09:33

      никогда не парсить бинарные данные с помощью приведения к типу указателя на __attribute__((packed)) структурки.

      А как тогда? Героически memcpy'ить каждое поле? Так это кода в несколько раз больше. И вот совсем не факт, что ошибок от этого станет меньше.

      махинации ещё могут нарушать требования к выравниванию типов

      Нет. Для этих «старых» архитектур компилятор для полей упакованных структур генерирует код, который читает их побайтно, а потом склеивает. Проверено на ARM7, который падает в data abort на unaligned access.


      1. KanuTaH
        13.10.2019 12:42

        Каждое поле memcpy'ить не нужно, но и делать reinterpret_cast какого-нибудь произвольного куска буфера char* на указатель на упакованную структуру тоже нельзя. Нужно выделить память именно под данную структуру (скажем, на стеке), тогда требования к выравниванию структуры в целом (а они обычно у компилятора есть даже для упакованных структур) будут соблюдены, и заmemcpy'ить в нее вышеупомянутый кусок буфера, ну и дальше просто работать с ней напрямую (а вот брать а затем разыменовывать указатели на конкретные поля упакованных структур в общем случае нельзя).


        1. naething
          13.10.2019 20:56

          тогда требования к выравниванию структуры в целом (а они обычно у компилятора есть даже для упакованных структур)

          Я всегда считал, что у упакованных структур нет требований к выравниванию. В этом, в общем-то, и весь их смысл. Если есть сомнения/паранойя, можно вставить в код
          static_assert(__alignof__(MyStruct) == 1);
          
          перед тем, как делать reinterpret_cast.


          1. KanuTaH
            13.10.2019 21:20

            Я бы не стал на это ставить. Компилятор может разместить её в памяти так, что одни её поля будут выравнены, а другие — нет, и генерировать специальный код только для доступа к тем полям, которые по его мнению не выравнены. Смысл упакованных структур все-таки не в том, чтобы можно было чихать на выравнивание ЛЮБЫХ полей, а в том, чтобы между полями не вставлялся паддинг.


            1. naething
              13.10.2019 21:42

              Согласен, что эта особенность плохо документирована, но все-таки ставить на это можно. При наличии сомнений можно использовать такую обертку:

              template<typename T>
              T* unaligned_cast(void* p) {
                  static_assert(__alignof__(T) == 1);
                  return reinterpret_cast<T*>(p);
              }
              


              1. KanuTaH
                14.10.2019 00:17

                Даже если и так, я не вижу в правилах применения reinterpret_cast такого случая, чтобы можно было безопасно выполнять такую конвертацию. Можно безопасно приводить (а затем безопасно разыменовывать) object pointer type к char*, но не наоборот.


            1. reishi
              14.10.2019 13:34

              Хотелось бы подробную статью со всеми подводными камнями и how to по данной теме


              1. KanuTaH
                14.10.2019 15:34

                Статей про алиасинг (а тут по сути речь о нем) в C и C++ по-моему уже миллион был на хабре, смысл писать миллион первую? :) Вот например:

                habr.com/ru/company/otus/blog/442554


      1. ilammy
        13.10.2019 12:51

        А как тогда? Героически memcpy'ить каждое поле?

        Именно. Держать мух отдельно от котлет. Бинарное on-the-wire представление — это отдельный формат. Удобный для языка программирования объект — это другой формат. Как абстрактный тип данных «строка» и её конкретное представление, скажем, в UTF-8.


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


        Для этих «старых» архитектур компилятор для полей упакованных структур генерирует код, который читает их побайтно, а потом склеивает.

        Хех, действительно, именно так и делает. Не знал, спасибо!


        Просто я травмирован undefined behavior sanitizer, который несмотря на всё это выдаёт предупреждения, когда так пытаешься делать.


        1. KanuTaH
          13.10.2019 13:03

          Просто я травмирован undefined behavior sanitizer, который несмотря на всё это выдаёт предупреждения, когда так пытаешься делать.

          Возможно, в вашем случае он не так уж и неправ. А вы попробуйте вместо каста, который вы по ссылке используете, сделать экземпляр упакованной структуры на стеке, заmemcpy'ить в нее целиком содержимое и потом с ней поработать. Скорее всего, никаких возражений у ub sanitizer'а это не вызовет.


    1. KanuTaH
      13.10.2019 12:06

      При доступе к полям структур с attribute packed компилятор гарантирует генерацию кода, который будет корректно работать в том числе с невыровненными полями. Но есть нюанс — эти гарантии распространяются только на прямой доступ. Если вы, скажем, возьмёте указатель на конкретное невыровненное поле, то доступ по этому указателю будет уже по "обычным" правилам.


    1. naething
      13.10.2019 20:47
      +3

      Но формально нарушение выравнивания является undefined behavior, и существуют архитектуры, где это фатально.
      Здесь нет никакого нарушения выравнивания. У упакованных структур выравнивание всегда равно единице, поэтому нарушить его невозможно. В этом весь смысл упакованных структур.

      Работает это не только на x86 и ARM. Если платформа не поддерживает невыровненные обращения к памяти, то для чтения/записи поля из упакованной стрктуры компилятор вставит соответствующие инструкции.

      Вот весь пост — прямо классическая иллюстрация причины, скрывающейся за правилом: никогда не парсить бинарные данные с помощью приведения к типу указателя на __attribute__((packed)) структурки.
      Приведенное вами «правило» необоснованно по изложенной выше причине. Упакованные структуры — весьма практичный способ разбора бинарных данных.


  1. dmxvlx
    13.10.2019 01:10

    У меня протокол поверх TCP, но нет, ничего такого не делаю.
    Со всеми полями работаю в host ordering, так что нет необходимости держать
    заголовок в двух представлениях(BE и LE) — сразу декодирую по приходу.


    А так да, — ваше решение хорошо иллюстрирует пример безопасного кодинга со строгой типизацией.


  1. BugM
    13.10.2019 02:09

    Убрать весь такой код подальше в общие библиотеки и покрыть тестами. Полностью покрыть. С кодревью тестов.

    Переписывать как можно реже. Желательно вообще никогда.


    1. kozlyuk Автор
      13.10.2019 10:13

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


  1. dipsy
    13.10.2019 06:01

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

    typedef uint wheels_count;
    typedef uint doors_count;
    doors_count y= 2;
    wheels_count x= y;// Вот это хотелось бы сделать невозможным на уровне компилятора.


    1. Aldrog
      13.10.2019 09:13

      Советую посмотреть на библиотеку NamedType.


    1. lamerok
      13.10.2019 09:54

      Если использовать статический анализатор, типа Lint, с правилами проверки строгой типизации, то он руганется на этом коде.
      Причём на двух строчках сразу, так как 2 литерал int и происходит неявное преобразование 2 к doors_count, aka unsigned int.


    1. PyerK
      13.10.2019 13:12

      Я себе написал простой шаблонный класс. Назвал like.
      Пользуюсь так

      struct wheel_count : like<uint>;

      Внутри есть автоматический оператор типа, поэтому получение uint'а прозрачно для программиста, а присвоение другого like<> типа невозможно.


      1. lamerok
        13.10.2019 18:53

        А зачем наследование? Можно же как alias использовать using wheel_count = like<int>;
        Или в структуре wheel_count ещё что-то определено?


        1. vitvakatu
          13.10.2019 20:29

          Если будет несколько like — то они между собой отличаться же не будут в вашем случае. А если заводить по отдельной структуре на каждый тип — то компилятор будет следить.


          1. PyerK
            13.10.2019 21:24

            Да, всё именно так. Может кто то знает более выразительный способ?


            1. Aldrog
              14.10.2019 11:17

              В библиотеке, на которую я выше приводил ссылку, для этого дополнительный шаблонный параметр заведён.
              NamedType<int, struct wheels_count_tag> и NamedType<int, struct doors_count_tag> — уже разные типы.


          1. lamerok
            14.10.2019 07:12

            Понял, спасибо.


      1. NickViz
        15.10.2019 13:27

        а можно его тут привести? а то попробовал сам изобразить — что-то бойлер-плейта много получается.


        1. PyerK
          15.10.2019 14:37

          Пишу по памяти:

          template<typename LikeWhat>
          struct like
          {
              LikeWhat value;
          
              operator LikeWhat()
              {
                  return value;
              }
          
              void assign( const LikeWhat& newVal)
              {
                  value = newVal;
              }
          };

          Из минусов, нету оператора присваивания и конструктора из исходного типа (они никак не смогут подтянуться из базового типа). Если они нужны, то прийдется применить ещё одну абстракцию что бы не писать бойлерплейт для них (а внутренний тип назвать impl например).

          template<typename T>
          struct assignable: T
          {
              assignable(const decltype (T::value)& arg)
              {
                  T::value = arg;
              }
          
              assignable& operator=(const decltype (T::value)& arg)
              {
                  T::value = arg;
                  return *this;
              }
          };


          тогда код будет таким:
          struct wheels_count_impl : like<uint> {};
          using wheels_count = assignable<wheels_count_impl>;
          struct doors_count : like<uint> {};
          
          
          int main(int argc, char *argv[])
          {
              wheels_count wc(3);
              wc = 2;
          
          // или
              doors_count dc;
              dc.assign( 4 );
          


          1. DistortNeo
            15.10.2019 15:04

            Из минусов, нету оператора присваивания и конструктора из исходного типа (они никак не смогут подтянуться из базового типа)

            Почему же?


            struct like
            {
                LikeWhat value;
            
                like(const LikeWhat& value)
                    : value(value) {}
            
                operator LikeWhat()
                {
                    return value;
                }
            
                void assign(const LikeWhat& newVal)
                {
                    value = newVal;
                }
            };
            
            struct wheels_count : like<int>
            {
                using like<int>::like;
            };
            
            void Test2()
            {
                wheels_count wc = 3;
                wc = 4;
            }

            С другой стороны, имеем неявное преобразование типа в обе стороны — а это может негативно сказаться на самом смысле использования строгой типизации. Проблема решается explicit конструктором:


            template<typename LikeWhat>
            struct like
            {
                LikeWhat value;
            
                explicit like(const LikeWhat& value)
                    : value(value) {}
            
                operator LikeWhat()
                {
                    return value;
                }
            
                void assign(const LikeWhat& newVal)
                {
                    value = newVal;
                }
            };
            
            struct wheels_count : like<int>
            {
                using like<int>::like;
            };
            
            void Test2()
            {
                auto wc = wheels_count(3);
                wc = wheels_count(4);
            }


            1. PyerK
              15.10.2019 17:29

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


    1. kdmitrii
      13.10.2019 18:52
      -1

      Имена переменных важнее их типов.
      doors_count x = 2
      намного хуже чем
      int doors_count = 2


      В коде перед глазами имена, а не типы. По сути auto и ввели чтобы не думать о том, что за тип, а по имени примерно догадаться какой у объекта интерфейс.


      1. lamerok
        13.10.2019 19:12

        А чтобы потом ошибок не было,

        auto cash = 1000;
        ....
        cash += 32000;
        

        На каком нибудь 16 битном или 8 битном процессоре и его компиляторе.
        Вы удивитесь, что ваш cash стал «немножко» не тем.
        А все потому что компилятор вывел за вас тип, в данном случае int, размер которого на компиляторах для 16 и 8 битных микроконтроллеров очень даже может быть 16 бит.

        Поэтому тип важен и лучше его явно указывать.
        uint32_t сash  = {1000U};
        

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


        1. kdmitrii
          13.10.2019 19:30
          +1

          1. Я не понимаю каким образом этот пример вообще коррелирует с моими словами. Я где-то сказал что типы не важны?
          2. Нужно указать явный тип — так указывайте. Кто мешает-то?
          3. Если наплодить типобезопасных алиасов для примитивов, как потом с ними работать? Как инкрементить, например? Как использовать в функциях типа std::cout или там std::to_string? Или они все будут неявно кастоваться? Зачем они тогда?
          4. Просто интересно зачем так странно проинициализирлван uint32_t?


          1. lamerok
            14.10.2019 07:08

            1.

            Я не понимаю каким образом этот пример вообще коррелирует с моими словами. Я где-то сказал что типы не важны?

            Возможно я вас неправильно понял, но
            Имена переменных важнее их типов.
            воспринялось мной именно так. Даже просто сравнение, что одно важнее другого наводит на мысль, что типы не так важны как имена, хотя это совершенно не так, так как из-за имен навряд ли вы ошибку получите, а вот из-за типов получите. Достаточно вспомнить ошибку преобразования из одного типа в другой в ПО для Ариант-5, даже на таком строго типизированном языке как Ada, где неявные преобразования запрещены почти для всех типов.

            2. Я имел ввиду, что для простых типов (которые компилятор неявно может преобразовать) тип не то, что хочешь или не хочешь — указывай, а необходимо указывать, иначе можно нарваться на неприятность.

            3. Под словом алиасы, вы наверное имели ввиду обертки над типами. Да придется скорее всего операторы у них переопределять. Либо как выше показано, И кастовать придется явно в таком случае, чтобы дать понять компилятору, что это ваша задумка и вы это делаете осознано, впрочем, даже явные касты — это не хорошо.

            4. Это «не сужающая» инициализация. Если вы не используете статический анализатор кода, то, в случае если вы напишете int f = 3.14 ;, компилятор сделает неявное преобразование double к int, и собственно не обязан вам об этом сообщать, хотя думаю, что современные компиляторы что-то вам скажут, но не все. Но на вот такое int f = {3.14} ; компилятор обязан выдать вам ошибку, ну или как минимум warning, что-то типа invalid narrow conversion. Скажет, что вы 3.14 aka double «обрезали» до типа f aka int, т.е. потеряли точность. В данном случае в uint32_t сash = {1000U}; это не особо пригодится, но, например, в таком случае int cash = {40 000} ; это будет очень даже будет осмысленно. Так как если int вдруг окажется 16 битным, то тут будет ошибка narrow conversion. Я уже просто так привык писать везде.


            1. Aldrog
              14.10.2019 11:10

              3.
              Добавлю ещё, что переопределение операторов для таких типов несёт дополнительный смысл: можно запретить операции, не имеющие смысла. Например, запретить умножать width на width, но разрешить умножать width на height, причём результатом такого умножения будет ещё один тип area.


              4.
              А почему не просто uint32_t сash{1000U};?


              1. lamerok
                14.10.2019 12:07

                3. Ага, точно

                4.

                uint32_t сash{1000U};

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


    1. BD9
      14.10.2019 00:13

      Осталось перейти на использование контрактного программирования и соответствующие языки (Eiffel или SPARK+Ada).
      (это была отчасти шутка)


  1. Hett
    13.10.2019 10:15

    Скорее в статье говорится про статическую типизацю.


    1. kozlyuk Автор
      13.10.2019 10:16

      Нет: https://en.wikipedia.org/wiki/Strong_and_weak_typing.
      C++ и так статически типизированный. Речь именно о запрете некоторых неявных преобразований.


      1. Hett
        13.10.2019 18:07

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


  1. MinimumLaw
    13.10.2019 11:17
    +1

    Лучший вариант решения этой проблемы (из всех видимых мной) реализован на чистом C в ядре Linux. Вполне допускаю что не только в Linux, но и в других UNIX. Типы __le16 и __be16, а так же __le32 и __be32 совершенно четко избавляют от описанных проблем. Просто в нужных структурах используются нужные типы. Вот только с переносимостью такого решения (особенно между разными компиляторами на разных платформах) есть проблемы.


  1. atbuhw
    13.10.2019 14:14

    Полностью поддерживаю идею о борьбе с багами таким способом с помощью строгой типизации.

    Думаю, тот же подход можно применить и к борьбе с SQL-инъекциями, например (если код, обращающийся к SQL написан на C++, конечно). То есть если мы (условно) пишем

    bool ok = check_for_special_characters (input);
    if (ok) 
    {
    std::string sql_request = std::string("SELECT ") + input;
    ...
    

    или
    std::string sql_request = std::string("SELECT ") + escape(input);
    , то компилятор не может это отличить от
    std::string sql_request = std::string("SELECT ") + input;

    А если бы у нас было 2 разных типа class SqlRequestString и std::string, то компилятор ругался бы на их тупую конкатенацию.


    1. flapenguin
      13.10.2019 15:50

      Для sql уже есть готовое решение всех этих проблем и оно лежит в другой плоскости. Надо использовать prepared statements, а не конкатенировать строки.


      1. atbuhw
        13.10.2019 16:07

        Мне кажется, это не совсем «в другой плоскости», класс PDOStatement из php по защитной функциональности аналогичен гипотетическому классу SqlRequestString из моего комментария (я не претендую на то, что первым придумал эту идею, на мой взгляд, идея такой защиты достаточно очевидна).


        1. playermet
          14.10.2019 22:40

          Мне кажется, это не совсем «в другой плоскости», класс PDOStatement из php по защитной функциональности аналогичен гипотетическому классу SqlRequestString из моего комментария
          Это проблема конкретно PDOStatement из php, который только эмулирует prepared statements. Ваш класс из примера всего-лишь добавляет простую проверку на тип. При этом вполне может возникнуть ошибка или уязвимость в функции escape, или пользователь класса что-то не поймет и силой преобразует строку в требуемый тип. А при создании трушного prepared statement в принципе не нужен escape, потому что забинденные к нему параметры никогда не участвуют в парсинге уровня sql-statement, ни на одном из этапов. Код запроса и параметры отделены друг от друга, и в таком виде принимаются СУБД. Это значит, что SQL-Injection через prepared statement физически невозможен, это примерно то же самое что пытаться заразить компьютер вирусом в плейн-текстовом файле.


      1. DistortNeo
        13.10.2019 23:50

        Увы, это не является решением проблемы. Что будете делать, если число параметров будет исчисляться тысячами?


        1. playermet
          14.10.2019 11:31

          Переписывать SQL-выражение.


          1. DistortNeo
            14.10.2019 11:52

            Ну-ка расскажите, как вы будете переписывать INSERT с большим числом записей или SELECT с IN (тут большое множество), желательно платформенно-независимым способом.


            1. playermet
              14.10.2019 16:27

              Ну-ка расскажите, как вы будете переписывать INSERT с большим числом записей
              Как одну транзакцию с многократно выполненным переиспользуемым prepared statement с INSERT. И это даже будет быстрее работать, потому что распарсить маленький запрос и подставлять готовые строки данных намного проще чем сначала отэскейпить дикую ораву текста, а потом ее же парсить.
              SELECT с IN (тут большое множество)
              IN поддерживает подзапросы в качестве аргументов. Очевидное решение — перенести параметры в таблицу.

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


              1. DistortNeo
                14.10.2019 16:40

                Как одну транзакцию с многократно выполненным переиспользуемым prepared statement с INSERT.

                Это банально долго, особенно если записей десятки тысяч.
                Конечно, можно использовать специфичные для каждой БД команды типа COPY FROM BINARY в случае PostgreSQL — будет эффективно, но непереносимо.


                IN поддерживает подзапросы в качестве аргументов. Очевидное решение — перенести параметры в таблицу.

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


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

                Аналог функции — это prepared statement, а не SQL-запрос. Потому prepared statement с тысячами пароаметров — это бред.


                1. playermet
                  14.10.2019 16:47

                  Это банально долго, особенно если записей десятки тысяч.
                  Нет. Как я уже дополнил выше, это будет работать быстрее, на некоторых базах (например, sqlite) — значительно.
                  Но что делать, если возможен одновременный доступ многих клиентов к базе?
                  Эмм, а что нужно делать? Или вы думаете что очередная туча эскейпов, конкатенаций, и парсинга будет быстрее чем закешированный и оптимизированный СУБД поиск IN по таблице? Если да, то вы опять же ошибаетесь.
                  Придётся создавать короткоживущую временную таблицу
                  Т.е. тысячи параметров еще и разные на каждый запрос? Ну тогда тут нужно искать ошибку на уровне архитектуры. Такой необходимости не должно возникать в 99.99% случаев.
                  Аналог функции — это prepared statement, а не SQL-запрос. Потому prepared statement с тысячами пароаметров — это бред.
                  Prepared statement это не более чем предраспарсенный SQL-запрос.


                  1. DistortNeo
                    14.10.2019 16:58

                    Нет. Как я уже дополнил выше, это будет работать быстрее, на некоторых базах (например, sqlite) — значительно.

                    Зависит от конкретной ситуации. Если база удалённая, то будет весьма долго и тоскливо вставлять по 1 записи — уже сталкивался с таким.


                    Эмм, а что нужно делать? Или вы думаете что очередная туча эскейпов, конкатенаций, и парсинга будет быстрее чем закешированный и оптимизированный СУБД поиск IN по таблице? Если да, то вы опять же ошибаетесь.

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


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

                    Конечно. Пример: фильтр огромного лога по параметрам.


                    Prepared statement это не более чем предраспарсенный SQL-запрос.

                    Да, с указанием мест, куда будут помещены параметры.


                    1. playermet
                      14.10.2019 17:11

                      Если база удалённая, то будет весьма долго и тоскливо вставлять по 1 записи — уже сталкивался с таким.
                      Не будет конечно. Оно отошлет все пачкой, если только вы не забыли про упомянутое мной оборачивание в транзакцию.
                      Парсинг команды относительно её выполнения во многих случаях занимает пренебрежительно мало времени.
                      Будет быстрее в любом случае. Выполнение есть и там и там. А вот поверх этого эйскейп, конкатенация, и парсинг только во втором случае. Нельзя выполнить работу быстрее чем ее же и еще несколько других.

                      Более того, парсинг списка это линейная сложность алгоритма в лучшем случае, а поиск в этом списке — в худшем. Обычно тут возникнет либо быстрый поиск по индексу, либо фулл скан который в среднем будет останавливаться просмотрев половину списка (при равномерном распределении вероятности найти элемент).
                      Пример: фильтр огромного лога по параметрам.
                      Извините, но можно подробней, что это за фильтр которому понадобилились тысячи параметров (да таких, что их нельзя свести к запросу по базе), и насколько подобная операция является повседневной и типовой, чтобы утверждать что «prepared statements не является решением проблемы»?
                      Да, с указанием мест, куда будут помещены параметры.
                      Что не меняет сути. Эти параметры можно заинлайнить. Так же как можно изменить api СУБД чтобы она принимала параметры в функцию выполнения SQL вместе со строкой с запросом. Ну т.е. они ортогональны друг другу по природе.


                      1. DistortNeo
                        14.10.2019 19:26

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

                        Можете привести пример? Я не нашёл функций в стандартных API, которые позволяют так делать. Только методы типа ExecuteNonQuery, но это вызов команд по очереди с ожиданием ответа. И транзакции тут вообще не при чём.


                        Нашёл только addBatch в Java, но т.к. я Java не пользуюсь, я не могу сказать, что там происходит под капотом.


                        Извините, но можно подробней, что это за фильтр которому понадобилились тысячи параметров (да таких, что их нельзя свести к запросу по базе), и насколько подобная операция является повседневной и типовой, чтобы утверждать что «prepared statements не является решением проблемы»?

                        Операция достаточно редкая, чтобы усложнять логику запросов, создавая временные таблицы и prepared statements.


                        Если рассматривать упрощённую модель, то лог — это набор записей с привязкой к ID объектов. Объектов много, и иногда требуется сделать фильтр по набору объектов, который определяется массивом ID.


                        1. playermet
                          14.10.2019 21:53

                          Можете привести пример? Я не нашёл функций в стандартных API, которые позволяют так делать.
                          Так никаких хитрых функций и не нужно (хотя возможно какие-то СУБД предоставляют соответствующие настройки). Просто начинаете транзакцию, делаете кучу инсертов, и заканчиваете транзакцию. Современные СУБД достаточно умны чтобы кешировать данные на запись и отправлять их крупными пачками.

                          Еще можно вспомнить специальный механизм Bulk Insert, но если нужна поддержка множества СУБД о ней можно сразу же и забыть.
                          Если рассматривать упрощённую модель, то лог — это набор записей с привязкой к ID объектов. Объектов много, и иногда требуется сделать фильтр по набору объектов, который определяется массивом ID.
                          Допустим. Вряд ли многотысячный список ID программист создает ручками каждый раз с нуля. Он либо генерируется по каким-то критериям, либо хранится и периодически пополняется со временем. В первом случае можно добавить необходимые критерии в базу, и делать по ним запрос. Во втором случае можно перенести хранение списка фильтруемых ID в базу.

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


                          1. DistortNeo
                            14.10.2019 22:30

                            Просто начинаете транзакцию, делаете кучу инсертов, и заканчиваете транзакцию.

                            Так это и будет медленно. Пусть база и кэширует данные на запись — это её дело, но бутылочным горлышком здесь может оказаться сетевой интерфейс между базой и клиентом (а точнее, latency). Каждый инсерт — это отправка команды по сети и ожидание ответа.


                            Он либо генерируется по каким-то критериям, либо хранится и периодически пополняется со временем.

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


                            1. dmxvlx
                              14.10.2019 22:44

                              Каждый инсерт — это отправка команды по сети и ожидание ответа.

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


                              1. DistortNeo
                                15.10.2019 00:09

                                Ответ не зависит от того, находитесь ли вы внутри транзакции или нет. Внутри транзакции вы видите состояние базы так, как если бы был автокоммит.


                                А так да, транзакции — это про фиксацию изменений в базе (всё или ничего) и изоляцию от других сессий.


                            1. playermet
                              14.10.2019 22:53

                              Каждый инсерт — это отправка команды по сети и ожидание ответа
                              В том то и дело, что нет, если транзакции использяются явно, или выключен режим autocommit.


                              1. KanuTaH
                                14.10.2019 23:03
                                +1

                                В смысле «нет»? Даже в рамках транзакции в приложении может работать логика, основанная на результатах выполнения каждого конкретного запроса, в том числе INSERT. Например, в postgresql ты можешь выполнить INSERT, потом через CmdTuples() посмотреть количество affected rows, а потом на основании этого принять решение, делать тебе следующий INSERT, COMMIT или вообще ROLLBACK. Но для этого, естественно, этот первый INSERT надо сначала таки выполнить.

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


                                1. playermet
                                  14.10.2019 23:15

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


                                  1. KanuTaH
                                    14.10.2019 23:17

                                    Ну так а откуда клиентская библиотека знает, так сказать, когда какой случай? Вот DistortNeo у вас и пытается узнать, о какой конкретно СУБД и клиентской библиотеке речь и как там включается режим пакетирования.


                                    1. playermet
                                      15.10.2019 00:28

                                      Ну так а откуда клиентская библиотека знает, так сказать, когда какой случай?
                                      Cкорость отправки по сети на много порядков (3-4 вроде) ниже чем скорость с которой данные будут приходить от prepared statement. Поэтому если вызывать достаточно простых insert подряд, они будут банально копиться в очереди, естественным образом. Ожидания результатов нет, очередь на запись большая — пакеты готовы.

                                      А если клиент что-то делает в зависимости от результата, он просто не вызовет следующий prepared statement, пока не дождется его, и очередь не будет пополняться.
                                      о какой конкретно СУБД и клиентской библиотеке речь
                                      Думаю, это так или иначе применимо для большинства популярных СУБД.


                                      1. DistortNeo
                                        15.10.2019 00:35

                                        Можете развернуть мысль более подробно? В какой очереди и где будут копиться insert? Не думали ли бы в том, что само помещение тысяч insert-ов в очередь будет занимать больше времени, чем выполнение записи в базу в самом конце?


                                      1. KanuTaH
                                        15.10.2019 00:36

                                        Ну а вы можете привести какую-то конкретику — что за клиент и к какой СУБД работает таким образом? Postgres'овская libpq например работает не так — там каждый PQexecPrepared() ждет результата выполнения запроса, ничего ни в какой очереди не копится.


                                        1. playermet
                                          15.10.2019 01:03

                                          Да, я что-то на ночь глядя тупанул. Подобный финт с батчингом сработает только если все инсерты части одной операции, ну и в либах где оно явно задается (addBatch/executeBatch в JDBC, например). Тем не менее, ускорение при использовании транзакции есть, иногда весьма заметное, что и ввело меня в заблуждение. Возможно просто этап закрытия/открытия транзакций слишком медленный сам по себе.


                          1. dmxvlx
                            14.10.2019 22:38

                            Просто начинаете транзакцию, делаете кучу инсертов, и заканчиваете транзакцию. Современные СУБД достаточно умны чтобы кешировать данные на запись и отправлять их крупными пачками.

                            Как такового кеширования нет. Создаётся новая версия строки в таблице, и после коммита (подтверждения записи) они либо появляются в таблице, либо так и исчезают (вроде бы ещё и прихватывая с собой авто-инкрементированные последовательности айдишников; врать не буду, но в теории: если отменить в конце транзакцию на 100 вставок, то следующая запись будет с id: OLD+100+1)


                            Оттого, что основная часть работы по закреплению данных производится на этапе коммита, то по скорости выходит выгоднее вставки/удаления/обновления в блоке транзакции: или 100 записей за раз распихать по БД + обновить индексы, или 100 раз по одной записи — время затрачиваемое на блокировку таблиц/столбцов/строк (в зависимости от реализации/стратегии блокировок) является узким местом.


                            1. DistortNeo
                              15.10.2019 00:18

                              Оттого, что основная часть работы по закреплению данных производится на этапе коммита, то по скорости выходит выгоднее вставки/удаления/обновления в блоке транзакции: или 100 записей за раз распихать по БД + обновить индексы, или 100 раз по одной записи — время затрачиваемое на блокировку таблиц/столбцов/строк (в зависимости от реализации/стратегии блокировок) является узким местом.

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


                              Каждый INSERT — это отправка данных по сетевому соединению и ожидание ответа от базы ("команда получена и выполнена"). Пинг 2 мс при вставке тысячи строк отдельными инсертами выльется минимум в 2-секундную задержку. Если же все данные объединить в один INSERT, то задержка будет не больше 10 мс.


                              Аналогия: что быстрее — скачать 1000 файлов по 1 кб или скачать 1 файл размером в 1 мб?


                              Ну а сколько там будет пыжиться база, вставляя эти строки в таблицу, уже отдельное дело.


                              вроде бы ещё и прихватывая с собой авто-инкрементированные последовательности айдишников; врать не буду, но в теории: если отменить в конце транзакцию на 100 вставок, то следующая запись будет с id: OLD+100+1

                              Да, авто-инкременты не откатываются.


                              1. dmxvlx
                                15.10.2019 00:43

                                Если же все данные объединить в один INSERT, то задержка будет не больше 10 мс.

                                Кроме как сформировать текст запроса с 1000 инсертами и выполнить запрос одним разом, — не вижу способов.
                                Если вы что-то знаете об этом больше меня — не стесняйтесь, рассказывайте :)


                                1. DistortNeo
                                  15.10.2019 00:49

                                  Стоп! Какой текст запроса, если мы говорим об использовании prepared statements, которые как раз и используются для того, чтобы не делать запросы текстом?


                                  Вызов prepared statement — это вообще отдельная команда БД, и параметры там обычно передаются в бинарном виде.


                                  Если вы что-то знаете об этом больше меня — не стесняйтесь, рассказывайте :)

                                  Ну вот в PostgreSQL есть способ бинарного импорта/экспорта, в других БД тоже наверняка имеются варианты. А вот универсального варианта, кроме как городить либо 1000 инсертов (пусть и с prepared statements), либо один большой, нет.


                                  1. dmxvlx
                                    15.10.2019 01:18

                                    Ну prepared так prepared, сути дела это не меняет.


                                    Вызов prepared statement — это вообще отдельная команда БД, и параметры там обычно передаются в бинарном виде.

                                    Ну эээ, я как бэ знаю что такое prepared statement.


                                    А вот универсального варианта, кроме как городить либо 1000 инсертов (пусть и с prepared statements), либо один большой, нет.

                                    Делайте так, как считаете нужным.


              1. BugM
                15.10.2019 01:18

                IN поддерживает подзапросы в качестве аргументов. Очевидное решение — перенести параметры в таблицу.

                Типовой сценарий. Сверяем табличку со внешним источником.
                Табличка большая, внешний источник большой. Для упрощения и там и там одна колонка ID.
                Сверка раз в сутки, внешний источник за сутки изменяется непредсказуемо и неконтролируемо. Наша sql табличка в течении суток тоже меняется. Нужны записи которые есть во внешнем источнике и нет в sql табличке. Ожидаемый результат: таких записей вообще нет или их очень мало.

                Писать в таблицу дорого. Запись на sql базах никак не масштабируется. Разделить внешние данные на чанки и прогнать кучу IN() быстрее всего.


  1. naething
    13.10.2019 21:14

    Когда я работал над реализацией протокола WiFi, мы тоже пришли к похожему решению:

    fuchsia.googlesource.com/fuchsia/+/refs/heads/master/src/connectivity/wlan/lib/common/rust/src/big_endian.rs

    В основном протоколе все поля в little endian, но иногда попадаются big endian из верхних слоев.


  1. monah_tuk
    14.10.2019 10:20

    У вас использование bswap() в таком виде, как в статье используется и в реальном коде? Не думали над ситуацией "ARM/PowerPC/MIPS в режиме BE"? Т.е. у вас нет никакой проверки времени компиляции, для какой последовательности байт собираетесь?


    1. kozlyuk Автор
      14.10.2019 23:40

      Сейчас мы ориентируемся только на x86 + я для статьи низкоуровневые вещи вытащил из отдельного файла ближе к телу.


      1. monah_tuk
        15.10.2019 01:17

        Понял, просто мне было интересно, если вы делали проверку, как это реализовывали. А то честный способ с возможностью constexpr завезли только в C++20. Остальное или в рантайме, или на макросной магии с компилятор-специфичными декларациями и предположениями.


  1. kotlomoy
    14.10.2019 23:25

    Привет вас из мира сурового C. Тоже недавно начал строгую типизацию для борьбы с неправильным порядком аргументов при передаче в функцию. Пока велосипед выглядит примерно так:

    #define DECLTYPE_ALIAS(alias, base_type)     typedef struct { base_type _; } t_##alias;     t_##alias alias( base_type const _ );
    
    #define CONSTRUCT_ALIAS(alias, base_type)     t_##alias alias( base_type const _ ) { t_##alias Ret; Ret._ = _; return Ret; };
    
    DECLTYPE_ALIAS( kurs, double );
    DECLTYPE_ALIAS( tang, double );
    DECLTYPE_ALIAS( kren, double );
    
    CONSTRUCT_ALIAS( kurs, double );
    CONSTRUCT_ALIAS( tang, double );
    CONSTRUCT_ALIAS( kren, double );
    
    t_carrier_angles carrier_angles( t_kurs const Kurs, t_tang const Tang, t_kren const Kren )
    {
        t_carrier_angles Ret = {{0},{0},{0}};
        Ret.Kurs = Kurs; Ret.Tang = Tang; Ret.Kren = Kren;
        return Ret;
    };
    
    t_carrier_angles const KursKrenTang = carrier_angles(
        kurs( ScenePacket->Carrier.psic_aircraft ),
        tang( ScenePacket->Carrier.tetac_aircraft ),
        kren( ScenePacket->Carrier.tamac_aircraft ) );
    

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