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

В С++ существует четыре оператора, осуществляющих приведение типов. Это static_cast, dynamic_cast, const_cast, и reinterpret_cast.
По наследству из языка C нам еще досталась возможности кастить переменные так называемым C-like кастом, это такие круглые скобочки с типом внутри. За использование такого каста Мартин предлагал сразу отрубать руки, и если-бы менеджмент пошел ему навстречу, то половина нашей фирмы уже давно ходила-бы без рук. Что-бы хоть как-то руки уберечь, рассмотрим по порядку, что нам предлагает C++.

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

const_cast - наш следующий дружок. Он посложнее чем static_cast и несет в себе небольшие грабельки. Как нам сообщает документация, этот вид кастинга существует для того, что-бы сносить константность переменной, например, можно снести константность переменной внутри константного метода. Другими словами такое легальное средство взлома. Скажем получила функция константный this, а мы его хряпнули, и он уже никакой не конст. Не знаю, кому это может понадобиться, надо спросить у Мартина.

Еще одно полезное извращение, связанное с ним, это передача константы в функцию, которая не собиралась принимать константу. Частенько это функции какой-нибудь древней библиотеки, которую уже никто никогда не поправит. Что из этого может получиться, остается только гадать. Поэтому, дети, пишите в сигнатурах своих функций слово const, не прогадаете.
Особенно радует непредсказуемое поведение, если мы снесем константность у переменной, которая была константной по рождению, и после этого попробуем ее модифицировать, ведь константной ее кто-то сделал не зря. Он хотел облегчить жизнь компилятору, тот как ребенок поверил и запихнул ее в надежное место, а мы ее чик и модифицировали.
Разные по типу объекты, к слову, не кастятся, из чего мы делаем вывод, что этот каст то-же происходит в момент компиляции. Некоторые просветленные умы сносят этим кастом атрибут volatile, а кстати что это за хрень? Одним словом использование этого каста явно указывает на проблемы с сигнатурами.

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

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

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

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


  1. qw1
    01.05.2024 19:43

    Неплохо бы объяснить отличие bit_cast от reinterpret_cast


  1. geher
    01.05.2024 19:43

    const_cast - ... Не знаю, кому это может понадобиться, надо спросить у Мартина.

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

    А вот интересно всегда было узнать отношение прогрессивной обществености к касту вида тип(значение). Это вроде не c style, который (тип) значение, компилятор предупреждений не выдает, но все равно вопрос есть, является ли он нежелательным, или таки дозволен наравне с новыми конструкциями?


    1. fujinon
      01.05.2024 19:43

      Какие же они новые) Отцы С++ уже успели от них отречься)

      https://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines.html#es48-avoid-casts

      (см. ES.48: Avoid casts)


      1. geher
        01.05.2024 19:43

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

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


        1. mayorovp
          01.05.2024 19:43

          Там же, чуть дальше: ES.49: If you must use a cast, use a named cast


    1. OldFisher
      01.05.2024 19:43
      +1

      А вот интересно всегда было узнать отношение прогрессивной обществености к касту вида тип(значение).

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


      1. geher
        01.05.2024 19:43

        Синтаксически это конструирование нового анонимного значения, то есть настолько безопасно, насколько вообще возможно

        Синтаксический смысл-то понятен. Вопрос был именно по взгляду прогрессивной общественности не факт использования. Вроде получается, что вместо него теперь рекомендуют конструкцию тип{значение} (с фигурными скобками вместо круглых), а если нельзя, то пользовать новые касты.


        1. fujinon
          01.05.2024 19:43

          Не так, вам выше уже написали что надо использовать named cast (то, что вы называете новыми кастами). Фигурные скобки -- только как альтернатива -- в случае отсутствия потери информации:

          Note When converting between types with no information loss (e.g. from float to double or from int32 to int64), brace initialization might be used instead.
          double d {some_float};
          int64_t i {some_int32};

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


    1. eptr
      01.05.2024 19:43

      А вот интересно всегда было узнать отношение прогрессивной обществености к касту вида тип(значение).

      Если значение — одно, то это — полный эквивалент C-style cast'а, то есть, ничем от него не отличается.

      Почитать можно здесь (пункт 2, первый подпункт в Explanation): Explicit type conversion.
      А именно:

      • If there is exactly one expression in parentheses, this cast expression is exactly equivalent to the corresponding C-style cast expression.

      А C-style cast expression можно использовать, в том числе, и для получения к доступа к static_cast'у с расширениями (пункт 1, подпункт b по той же ссылке).


  1. dyadyaSerezha
    01.05.2024 19:43
    +2

    А за использование дефиса где-ни-по-падя Мартин не предлагал отрубать руку?)


  1. SpiderEkb
    01.05.2024 19:43

    Но если уж совсем честно - любой каст это кривые руки архитектора или программиста.

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

    И как тут обойтись без каста байтового буфера в нужную структуру данных?


    1. Borjomy
      01.05.2024 19:43

      Мне вот тоже интересно, как в C++ делать преобразование "структура"->"строка" и "строка"->"структура" включая динамические массивы и строки, с проверкой на консистентность и обработкой ошибок?

      В LabVIEW, которым я пользуюсь, этот функционал встроенный, называется "flatten to string" и "unflatten from string". Например для передачи строки сначала передается ее длина (UINT32, 4 байта). Если массив, то размерности массива. И так по иерархии. Для восстановления структуры из строки функции "unflatten from string" передается тип структуры, по которой входящая строка разбирается. На выходе заполненная структура и остаток строки, ну и код ошибки, если содержимое строки не укладывается в предлагаемую структуру. Все это на этапе компиляции, т.е типовая структура - константа. Без этих функций для обмена между устройствами как без рук.


      1. SpiderEkb
        01.05.2024 19:43

        Это хорошо когда такое реализовано на этапе компиляции. Но в рантайме, особенно если речь идет о каком-то реалтайме, это непозволительно долго.

        Кастинг плох не сам по себе. Он плох тем, что там легко ошибиться (работа с указателями вообще потенциально более опасна чем работа с обычными переменными). Бездумное увлечение кастингом везде и всюду однозначно нехорошо. Но ограниченное, разумное и аккуратное использование его как инструмента для обеспечения производительности вполне оправдано.

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

        Насчет "старого" способа "скобочки-тип" - вообще не понимаю. Если вы против него - декларируйте что такая конструкция при компиляции автоматически превращается в static_cast. Или dynamic_cast. Всегда. Со всеми вытекающими. И проблем не будет.


      1. mayorovp
        01.05.2024 19:43

        Легко, просто дождитесь пока в стандарте наконец-то появятся метаклассы (ждём с 2017 года)… ну или воспользуйтесь внешними инструментами для кодогенерации. Можно начать с Protocol Buffers

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


        1. SpiderEkb
          01.05.2024 19:43

          Вы серьезно? Вот это все на стороне STM32 оформлять (это контроллер верхнего уровня - он собирает данные с 10-30 контроллеров нижнего уровня, а те, в свою очередь, с подключенных к ним устройств) и гонять через RS-485 какой-нибудь?

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

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


          1. eao197
            01.05.2024 19:43
            +1

            И ради чего все это? Вот чтобы что?

            Чтобы в коде не было UB и чтобы очередная версия компилятора не превратило результат трансляции вашего cast-а в ХЗ что.


            1. SpiderEkb
              01.05.2024 19:43

              Ну вообще такой себе аргумент... Если очередная версия компилятора превращает работающий код в ХЗ что, то нужен ли такой компилятор?

              Ну и UB со стороны компилятора когда разработчик на 146% знает что он делает и зачем, а компилятор считает себя слишком умным и "правит" его код по своему усмотрению - это, извините, хреновый компилятор.

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

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

              Чтобы что-то ровно и тонко порезать нож должен быть острым. А вот порежешься ты им или нет - это уже от твоих навыков зависит.


              1. eao197
                01.05.2024 19:43

                Если очередная версия компилятора превращает работающий код в ХЗ что, то нужен ли такой компилятор?

                Это перпендикулярный вопрос. Пока же мы живем в реальности, в которой в стандарте C++ определен ряд UB и компиляторам разрешено эти UB эксплуатировать. Что и происходит и, временами, ведет с потере работоспособности кода и последующим бурлением говн в этих наших Интернетиках.

                ЕМНИП, одним из UB, в частности, было то, что до C++20 вот такой вот код:

                alignas(DataHeader) char buffer[sizeof(DataHeader)];
                read_data(buffer);
                DataHeader * header = reinterpret_cast<DataHeader *>(buffer);
                

                содержал в себе UB.

                Поскольку эта практика была распространенной, такой UB не эксплуатировался компиляторами. Но в будущем запросто может. Поэтому в C++20 этот UB устранили для нескольких случаев (включая показанный в примере выше), но для других случаев подобные касты все равно остаются UB.


                1. zzzzzzerg
                  01.05.2024 19:43
                  +1

                  Если кому-то интересно, то смотреть сюда - P0593R6: Implicit creation of objects for low-level object manipulation (open-std.org)


              1. mayorovp
                01.05.2024 19:43
                +1

                Возможно, компиляторы С++ и правда хреновые - но других не завезли.

                Если разработчик на 146% знает что делает - это всё ещё не повод оставлять UB в коде. Как минимум, "знающий что делает" разработчик должен ещё объяснить компилятору что именно он делает, чтобы компилятор тоже знал.


                1. SpiderEkb
                  01.05.2024 19:43

                  Возможно, компиляторы С++ и правда хреновые - но других не завезли.

                  Вот поэтому критичные по скорости куски кода писали на С. Без вот этого всего вот. И некоторые модули компилировались с выключенной оптимизацией.

                  И таки да, я очень рад что последние 6+ лет мне не нужно со всем этим работать :-)


                  1. mayorovp
                    01.05.2024 19:43

                    И некоторые модули компилировались с выключенной оптимизацией.

                    Так, может быть, нехватка производительности была именно из-за этого отключения оптимизации?


                    1. SpiderEkb
                      01.05.2024 19:43

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

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

                      Поэтому окончательное тестирование на стендах проводилось на достаточно слабых компах.

                      В общем, дело прошлое уже. И слава богу. Сейчас другой стек, другой язык (С/С++ очень иногда и в ограниченных количествах).


                      1. mayorovp
                        01.05.2024 19:43
                        +1

                        Вот просто не верю, что выключенная оптимизация вам помогала в работе на слабом железе, а не мешала.


                  1. eao197
                    01.05.2024 19:43
                    +1

                    Вот поэтому критичные по скорости куски кода писали на С.

                    Если вы думаете, что в чистом Си нет UB, то вам нужно прочитать хотя бы вот эту серию статей: https://habr.com/ru/articles/341048/ (это первая, в ней ссылки на остальные).


                    1. SpiderEkb
                      01.05.2024 19:43

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

                      "На той стороне" в этот байтовый буфер была положена некая структура. И "на этой стороне" мы рассматриваем этот же байтовый буфер именно как эту структуру. До тех пор, пока сюда не вмешивается оптимизатор, все хорошо. Потому что порядок следования байт и выравнивание мы устанавливаем руками одинаковое с обеих сторон.

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

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


                      1. eao197
                        01.05.2024 19:43
                        +1

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

                        Оптимизатор не может стать причиной UB. Оптимизатор может воспользоваться оставленным пользователем UB. Например, в C++ (если я не запутался в нововведениях), вы не можете просто так написать код вроде:

                        char data[1024];
                        read_data(data);
                        some_struct * payload = reinterpret_cast<some_struct *>(data + 53 /*пропустили заголовок*/);
                        

                        В чистом Си можете, а в C++ начиная с C++23 для этих целей следует применять std::start_lifetime_as.

                        Потому что порядок следования байт и выравнивание мы устанавливаем руками одинаковое с обеих сторон.

                        У "выравнивания" есть две составляющие в данном вопросе:

                        Первое: упаковка данных внутри буфера. Так, если у вас структура вида:

                        struct header {
                          uint32_t _magic_number;
                          uint8_t _version;
                          uint16_t _fields;
                          uint64_t _flags;
                        };
                        

                        то без #pragma pack для вашей структуры в зависимости от настроек компилятора между _version и _fields может быть "дыра".

                        Второе: выравнивание адресов для вашей структуры на принимающей стороне. Тот самый alignas из современного C++. Без оного фокус вида:

                        char data[sizeof(header)];
                        read_data(data);
                        header * h = (header *)data;
                        if(0xDEADBEAF == h->_magic_number) // OOPS!
                        

                        может аварийно завершится на некоторых платформах в точке "OOPS".

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

                        В компиляторах, конечно же, встречаются ошибки. Но, как правило, если программа в релизе с оптимизациями не работает, то с 99% вероятностью это проблема кода пользователя, а не качества компилятора.


                      1. SpiderEkb
                        01.05.2024 19:43

                        char data[1024];
                        read_data(data);
                        some_struct * payload = reinterpret_cast<some_struct *>(data + 53 /*пропустили заголовок*/);

                        Ну, блин. Это не UB. Это просто явный косяк. Который возникает от непонимания того, что адрес должен быть выровнен. Хотя тут от пллатформы зависит еще. Не все платформы требуют выравниваяния адресов. И не всегда.

                        struct {
                          char header[12];
                          char data[0];
                        } some_struct;
                        
                        p1 = (some_struct*)new char(64);
                        p2 = (data_struct*)&p1->data;

                        в С работает кроме случаев когда есть явное требование чтобы p2 был выровнен, скажем, на 16.


                      1. eao197
                        01.05.2024 19:43
                        +2

                        Ну, блин. Это не UB. Это просто явный косяк.

                        Вы не за то зацепились. Проблема не в значении 53. Проблема в приведении типа. В чистом Си вот это не UB:

                        some_struct * p = (some_struct *)(some_ptr + some_offset);
                        

                        тогда как в C++ этот же код:

                        some_struct * p = (some_struct *)(some_ptr + some_offset);
                        

                        будет иметь UB. Этот UB не эксплуатировался компиляторами до C++20 (скорее даже до С++23), тогда как начиная с C++23 никаких гарантий по этому поводу уже нет. Не поставил std::start_lifetime_as, ну значит ССЗБ.


                      1. SpiderEkb
                        01.05.2024 19:43

                        В чем причина UB?

                        Вот смотрите. Как это лежит в памяти? Скажем, 64 байта. В памяти ему все равно что это такое. Там все в байтиках лежит. Первый, второй и так далее. Это мы знаем что первые 12 байт - это какой-то там "заголовок", а остальное - какие-то там "данные". А оно не знает.

                        Это нам надо с этими байтиками работать как с некоей структурой. И это мы для этого говорим себе что вот эти байтики нужно воспринимать как структуру. А оно не знат.

                        Итак - откуда тут берется UB? Оно может появится только от компилятора. Который почему-то решил, что раз мы создали это объект как массив байт, то и работать с ним надо как с массивом байт. Ну и оптимизировать посильно. Под работу с массивом байт. А мы взяли и скастили его под какую-то структуру. И компилятору от это плохо стало.

                        Я вот сейчас работатю с языком, где указатели нетипизированы (два типа - указатель на процедуру и указатель на данные). И никакой адресной арифметики. ptr += 1 дает смещение ptr ровно на один байт. Всегда.

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

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


                      1. mayorovp
                        01.05.2024 19:43
                        +2

                        В чем причина UB?

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

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

                        Чтобы указателем стало можно пользоваться - надо сначала создать объект в том месте, куда он указывает (сохранив данные, разумеется). Это и делает функция std::start_lifetime_as.


                      1. SpiderEkb
                        01.05.2024 19:43

                        Т.е. "точка зрения стандарта" уже главнее того, что физически происходит в памяти процесса?


                      1. mayorovp
                        01.05.2024 19:43

                        Т.е. "точка зрения стандарта" уже главнее того, что физически происходит в памяти процесса?

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


                      1. eao197
                        01.05.2024 19:43
                        +1

                        В чем причина UB?

                        Насколько я понимаю, причина в том, что компилятор C++ должен уметь отслеживать начало и завершение времени жизни объекта. Например:

                        some_struct s1; // Здесь очевидное начало lifetime для объекта.
                        some_struct * p1 = new some_struct{}; // Здесь еще одно очевидное начало lifetime.
                        some_struct * p2 = new(&s1) some_struct{}; // Здесь очевидное начало жизни для p2.
                        

                        В случае, когда мы тупо делаем reinterpret_cast или C-style case, время жизни ни для какого объекта не начинается.


                      1. SpiderEkb
                        01.05.2024 19:43

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

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


                      1. eao197
                        01.05.2024 19:43
                        +2

                        Но тут встает вопрос - не слишком ли много на себе берет компилятор?

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

                        Это C++, таков путь.

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

                        Начиная с C++20 компилятор сам уже достаточно умен, чтобы понимать в некоторых случаях, что пользователь неявно начинает время жизни для объекта. Ссылка на пропозал была выше. Начиная с C++23 для этих целей завезли start_lifetime_as.

                        На самом деле (жирное ИМХО) это был косяк комитета: эту практику, сколь уж она считалась UB, давным-давно нужно было узаконить. Но руки дошли только в рамках работ над C++20.

                        До этого многие (включая меня самого) даже не знали, что такое поведение является UB.


                      1. SpiderEkb
                        01.05.2024 19:43

                        К слову сказать, писалось все это под очень старые компиляторы. В разное время (проект был достаточоно длинный и начинался когда аналогов в стране просто не было - 92-93гг) использовались MSVC 1.52, MSVC 2.0, Borland C++ Builder 6 (этот своей жизнью жил вообще со своим VCL от дельфи, _property - реально удобная штука и поддержкой 10-байтового long double).

                        Ну а дальше я оттуда ушел и от С++ отошел. Поглядываю на то, что там творится - ну как-то многовато вопросов "кто мы, зачем мы и куда мы идем" возникает. Не, если потребуется - вникну и буду пользоваться, тут вопросов нет.


                      1. qw1
                        01.05.2024 19:43
                        +1

                        И могу ли я сказать компилятору, что я сам готов следить за временем жизни своих объектов и не надо это делать за меня

                        Точечно - можете, это и есть обмазывание кода всякими std::start_lifetime_as и std::launder
                        Глобальной настройки, типа "верни меня в мой 98-й" нет. Ну, разве что, использовать компиляторы 98-го года...


                      1. geher
                        01.05.2024 19:43
                        +1

                        Итак - откуда тут берется UB? Оно может появится только от компилятора.

                        UB берется от нелогичного стандарта, который жертвует логикой в пользу скорости компиляции и разных мутных оптимизаций. А компилятор конкретный уже решает, что ему с этим UB делать. Большинство компиляторов под микроконтроллеры (и некоторые под "обычные" системы) решают UB в пользу логичного поведения. Но гарантий, что такое поведение сохранится в будущем, увы нет. Потому, зная поведение конкретного компилятора можно написать нечто, эксплуатирующее это поведение, но по факту являющееся UB, но нужно полностью отдавать себе отчет в том, что на другом компиляторе оно может сломаться.


          1. mayorovp
            01.05.2024 19:43
            +1

            Вы серьезно? Вот это все на стороне STM32 оформлять

            Исходно вы говорили про получения данных с контроллера, а не на контроллере...

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

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

            И ради чего все это? Вот чтобы что? Все это очень хорошо работает когда у вас быстрые каналы связи

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


            1. SpiderEkb
              01.05.2024 19:43

              Исходно вы говорили про получения данных с контроллера, а не на контроллере...

              И на контроллере тоже.

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

              Вообще-то не бесплатная. Увы, но все эти грабли уже наступлены и пройдены.

              При этом сериализация не приносит никакой функциональности. Это просто защита от кривого компилятора и кривых рук разработчика. От того самого хирурга, которому рискованно давать нормальный скальпель - порежется. Но значит ли это что этот самый хирург должен оперировать безопасным пластиковым ножиком, или его нужно просто оправить поучиться владеть инструментом?

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

              Вообще-то ситуация такая - есть 30 контроллеров. Все они шлют в одно место данные. На контроллере стоит таймаут - послал данные, ждем, не пришел ответ - посылаем опять. Т.е. в этом таймаут входит все - время на доставку сообщения адресату, время обработки сообщения адресатом, время доставки подтверждения от адресата.

              Чем больше время на доставки туда-сюда, тем меньше его остается на обработку адресатом. Плюс адресат обрабатывает сообщения не от одного, а от 30-ти (условно, может и 50 быть) контроллеров. Т.е. там тоже очередь может быть в какие-то моменты.

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


              1. mayorovp
                01.05.2024 19:43
                +1

                Повторюсь: если на приём сообщений ставить не калькулятор - не вижу проблем их все принять и десериализовать.

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


                1. SpiderEkb
                  01.05.2024 19:43

                  Повторюсь: если на приём сообщений ставить не калькулятор - не вижу проблем их все принять и десериализовать.

                  Написал выше. Объяснять заказчику что нужно под это закупать еще и мощный комп - почти наверняка его потерять.

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

                  Кстати, а дальше-то что с сообщениями делалось?

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


                  1. mayorovp
                    01.05.2024 19:43

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


                    1. SpiderEkb
                      01.05.2024 19:43

                      А 10-20-30? Т.е. каждому клиенту ("мощный компьютер в подарок")? И прибыль там и так на пределе окупаемости... Начнете задирать цену - просто купят не вашу систему, а конкурента...


                      1. mayorovp
                        01.05.2024 19:43
                        +1

                        А нахера в этой задаче 10-20-30 компов?

                        Т.е. каждому клиенту ("мощный компьютер в подарок")? И прибыль там и так на пределе окупаемости... Начнете задирать цену - просто купят не вашу систему, а конкурента...

                        Это вы какую-то вообще жуткую ситуацию рассказываете. Нет, понятное дело, если припрёт - придётся и за еду работать, оптимизируя что можно и что нельзя на старом железе; однако прямо сейчас заказчика, который срывается к конкуренту из-за 30 тыс. рублей, я бы этому конкуренту и отдал. А то ведь, оптимизируя такты процессора немудрено и сроки затянуть и работать в итоге вообще в минус.


                      1. SpiderEkb
                        01.05.2024 19:43

                        А нахера в этой задаче 10-20-30 компов?

                        Потому что это система, которая делалась не для одного конкретного заказчика, а для продажи "коробочных" версий. Т.е. "тираж". И в те времена, когда какой-нибудь Celeron B820 с двумя ядрами 1.7ГГц и 2-4Гб памяти считался вполне нормальным рабочим компом. А что-то мощное, которое все эти сериализации спокойно вытянет, уже стоило очень ощутимо. И заказчики начинали нос воротить "а вот у конкурентов ваших нет таких требований, может нам лучше к ним пойти?"

                        Ставилось все это на диспетчерские УК - система диспетчеризации (мониторинг инженерного оборудования зданий). Там на диспетчерскую могло лететь с 20-30 контроллеров верхнего уровня. У каждого из которых 5-30 нижнеуровневых контроллеров. На каждом из которых куча устройств.

                        Короче, на диспетчерскую одних лифтов 500-600 штук запросто. Плюс куча охраны служебных помещений (электрощитовые, машзалы лифтов). Плюс дублирование пожарной сигнализации. Плюс контроль клапанов дымоудаления. Плюс видеонаблюдение. Плюс (иногда) данные с домовых приборов энергоучета и контроля параметров ГВС/ХВС/Отопления. Плюс освещение подъездов.

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

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

                        Много там чего было. Там же еще с другой стороны интерфейсные клиенты висят. 2-5 штук разных И каждый со своей подпиской. И каждому что от контроллера пришло передать надо (еще разобраться что кому передавать).

                        Одна инициализация чего стоит - когда контроллер попросил "по полной" - себя и всех своих "подопечных". И вот "основной клиент" шлет ему 20-30 пакетов. В строго определенном порядке. И чтобы ни от какого другого клиента в это время туда ничего постороннего не вклинилось...

                        Мне сейчас намного проще. Несмотря что банк, центральные сервера, hiload и все такое.


                      1. mayorovp
                        01.05.2024 19:43

                        Я уж думал, вы сейчас какой-нибудь 286й процессор назовёте, а тут Celeron аж с двумя ядрами! Не вижу каким образом недостаточно оптимальное перекладывание байтов (т.е. десериализация) может оказаться проблемой на этом довольно мощном процессоре.

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

                        Там только кажется что все просто. А когда поваришься несколько лет... Там дикая логика на обработку [...]

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


    1. mayorovp
      01.05.2024 19:43
      +2

      И как тут обойтись без каста байтового буфера в нужную структуру данных?

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

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


      1. eao197
        01.05.2024 19:43

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

        Если у вас свежий C++, то в C++20 уже были сделаны некоторые послабления.
        А в C++23 завезли std::start_lifetime_as.
        Проблем с выравниванием данных это не решает (тут вы правы на счет переносимости), но хотя бы избавляет код от UB.


        1. mayorovp
          01.05.2024 19:43
          +1

          Да уже одно только требование переносимости кода на "тупоконечную" платформу разрушит все идеи по поводу хитрых кастов, и std::start_lifetime_as.ничем не поможет.


          1. eao197
            01.05.2024 19:43

            Эт да, но это одна из самых очевидных проблем, которую легко обойти, если делать преобразование представления прямо "по месту". Веселее с неочевидными, когда люди, например, не знают про выравнивание данных и директивы вроде #pragma pack.


      1. SpiderEkb
        01.05.2024 19:43
        +1

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

        Все структуры заранее известны - их там десяток разных типов. И никакого "исхитряться с кастами" там нет. Есть байтовый буфер и есть несколько типов структур четко заданной длины и содержания. Тип структуры и ее размер задан в заголовке. Основное требование - минимизировать размер передаваемых данных (там на определенных участках цепочки скорости 57600 с возможностью понижения в случае возникновения помех) и максимально ускорить обработку в условиях ограниченных вычислительных ресурсов.

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


        1. mayorovp
          01.05.2024 19:43

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

          там на определенных участках цепочки скорости 57600 с возможностью понижения в случае возникновения помех

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


          1. SpiderEkb
            01.05.2024 19:43

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

            А я такого и не говорил. Наоборот, говорил что это очень частный случай когда общие правила не работают.

            На таких "высоких" скоростях можно данные не только десериализовать

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

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

            Если, конечно же, принимает сообщения не калькулятор.

            Дался вам это "калькулятор". Не поверите, но бывают ситуации, когда приходится писать код и под заведомо слабое железо. И держать в голове тот факт, что ресурсы в момент выполнения на реальном железе будут ограничены. И аргумент "а у меня на 8-ми ядрах и 64Гб памяти все летает" не прокатят - вам просто скажут "ну купите нам 8 ядер и 64Гб памяти".

            Легко и просто писать например, зная что на сервере одноуровневая память и 12Тб физической оперативки. Ну создал пару статических массивов по 16Гб под кеши - никто слова не скажет - надо так надо.

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


            1. mayorovp
              01.05.2024 19:43
              +1

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

              Так что нет, вы не убедили меня что медленный канал связи как-то усложняет ситуацию.

              Дался вам это "калькулятор". Не поверите, но бывают ситуации, когда приходится писать код и под заведомо слабое железо. И держать в голове тот факт, что ресурсы в момент выполнения на реальном железе будут ограничены. И аргумент "а у меня на 8-ми ядрах и 64Гб памяти все летает" не прокатят - вам просто скажут "ну купите нам 8 ядер и 64Гб памяти".

              Дались вам эти 8 ядер и 64Гб! Нет, для десериализации не требуется ни столько ядер, ни столько памяти.


              1. SpiderEkb
                01.05.2024 19:43

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

                Увы, иногда нужна. И там по одному канал связи много чего летит. Пока вы одно сообщение обрабатываете, вам еще 5 прилетело. И все ждут ответа. Не ответите - вместо 5-ти будет уже 10. В виде дублей по две штуки на каждое. Пока их разгребаете - уже 30 и т.д. стало.

                Никогда не приходилось с сетью промкнтроллеров работать? Попробуйте - очень занимательное занятие. Особенно отладка в реалтайме доставляет.


                1. mayorovp
                  01.05.2024 19:43
                  +1

                  Ну не может на скорости 57600 прилететь ещё несколько сообщений во время обработки одного, а если и прилетит - то уже точно не из-за потери времени на десериализацию.


                  1. SpiderEkb
                    01.05.2024 19:43

                    Ну вам виднее. Я так понимаю, вы подобными вещами лет 20 уже занимаетесь, раз так уверенно говорите.

                    Я-то что, так, погулять вышел. "мимкрокодил", как говорится.