(Большинство) реализаций C++ предоставляют по крайней мере 8, 16, 32 и 64-битные знаковые и беззнаковые целочисленные типы. Существуют потенциально неудобные неявные преобразования, споры о неопределенном поведении при переполнении (некоторые считают, что это перебор, другие — что недобор), но по большей части эти языковые конструкции хорошо справляются с поставленными задачами. В новых языках, таких как Rust, этот замысел скопирован, но исправлены проблемы с преобразованиями и поведение при переполнении.

Тем не менее, я считаю, что здесь есть место для инноваций. Позвольте мне рассказать о трех новых семействах целочисленных типов, которые я хотел бы увидеть в C++.

Симметричные знаковые целые числа


Де-факто представление знаковых целых чисел на современном аппаратном уровне решается как дополнительный код (two’s complement). У положительных значений старший бит равен нулю, а у отрицательных — единице. Чтобы получить абсолютное значение отрицательного числа, инвертируйте все биты и прибавьте единицу.

Например, для 8-битного целого числа 42 — это 0b0'0101010: знаковый бит нулевой, остальные представляют 42 в двоичном формате. С другой стороны, -42 — это 0b1'1010110: если инвертировать все биты, то получится 0b0'0101001, прибавить единицу и снова 0b0'0101010, то есть, получится 42. Важно отметить, что 0b1'1111111 равно -1, а более глубокие отрицательные значения доходят до 0b1'0000000, что равно -128.

Заметили что-то интересное в последнем значении? Если вы выполните преобразование, то при инвертировании получите 0b0'1111111, а прибавление единицы приведет к 0b1'0000000 — переполнение в знаковом бите.

Это означает, что абсолютное выражение наименьшего значения больше абсолютного выражения наибольшего значения 0b0'1111111 или 127; отрицательных значений больше, чем положительных, потому что положительная половина, где бит знака не установлен, также содержит число ноль.

Мне очень не нравится эта асимметрия — она приводит к проблемным побочным явлениям во всех видах целочисленных API.

Для начала, abs(x) для целых чисел x не является тотальной функцией: abs(INT_MIN) не представима. Аналогично, x * (-1) тоже не является тотальной функцией: INT_MIN * (-1) переполняется. Еще забавнее становится в случае с делением: конечно, x / y не может переполниться, так как деление уменьшает значение, верно? Неверно, INT_MIN / (-1) переполняется (и выдает ошибку деления на ноль (!) на x86). Более того, INT_MIN % (-1) также приводит к неопределенному поведению.

Итак, вот чего я хочу: знаковое целое число, где INT_MIN == -INT_MAX, полученное путем перемещения минимального значения на единицу вверх.

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

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

В-третьих, вы получаете неиспользуемый битовый паттерн 0b1'0000000, старый INT_MIN, который вы можете интерпретировать как угодно. Хотя в принципе вы можете превратить его в какой-нибудь отрицательный ноль для дополнительной симметрии, пожалуйста, не делайте этого (вместо этого просто используйте дополнение единицы или знак величины). Вместо этого мы должны скопировать другую функцию, относящуюся к арифметике с плавающей запятой: not-a-number («не число») или NaN. Назовем её INT_NAN = 0b1'0000000.

Как и NaN с плавающей запятой, INT_NAN не является допустимым целочисленным значением. В идеальном мире оно также было бы липким битом, так что INT_NAN ¤ x == INT_NAN, но для эффективной работы с ним требуется аппаратная поддержка. Вместо этого, давайте просто скажем, что арифметика на INT_NAN – это неопределенное поведение; тогда программа очистки, работая в режиме отладки, может проверить это, вставляя тестовые утверждения.

Почему я хочу INT_NAN и, таким образом, добавляю дополнительное предусловие ко всей целочисленной арифметике?

Потому что мне очень нравятся контрольные значения.

Например, с INT_NAN можно иметь sizeof(std::optional) == int. Вместо того, чтобы хранить дополнительное логическое число для отслеживания, существует ли опция, мы можем просто хранить INT_NAN. Аналогично, закрытая хэш-таблица должна каким-то образом различать пустые и непустые записи. Наличие контрольного значения позволяет обойтись без дополнительных метаданных.

Теперь вам может не понравиться, что мы решили работать с контрольным значением. Что если вы хотите хранить INT_NAN в std::optional?

Ну, INT_NAN — это не число, так почему вы хотите хранить его в int? Только если вам нужно какое-то самостоятельное значение. Это похоже на контейнер NaN для значений с плавающей запятой — вы теряете возможность хранить (большинство) NaN, но повышаете эффективность хранения. Однако, в отличие от арифметики с плавающей точкой, где, например, 0/0 может привести к NaN, в моей модели ни одна арифметическая операция над целыми числами не может привести к INT_NAN, поскольку переполнение является неопределенным поведением. Поэтому вам действительно нужно избавиться от операций присваивания INT_NAN, чтобы ввести в код целочисленные NaN.

Вам может не понравиться, что я предлагаю арифметику на INT_NAN сделать неопределенным поведением, если вы в прошлом попадали под агрессивную оптимизацию компилятора. Однако НП в стандарте само по себе не является чем-то плохим; НП буквально означает, что в стандарте не предъявляется никаких требований к поведению, что обеспечивает максимальную свободу компилятору. Можно исходить из того, что такого не происходит, выстраивая оптимизацию соответствующим образом, но также можно вставлять отладочные утверждения (либо отлавливающие все, либо обеспечивающие жесткие проверки с ложными отрицаниями), либо задать компилятору четко определенное поведение. Большинство распространённых компиляторов по умолчанию выбирают первую интерпретацию, но, например, я сейчас работаю над интерпретатором языка C, где компилятор будет выдавать панику во всех случаях неопределённого поведения.

Беззнаковые целые числа без одного бита


Использование беззнаковых целочисленных типов в C++ — дело, мягко говоря, спорное. «За» такой подход возможность выразить в системе типов тот факт, что некоторая сущность не может быть отрицательной. Аргументом «против» является тот факт, что вычитание легко приводит к возникновению больших значений из-за целочисленного переполнения при нуле.

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

Будучи сторонником беззнаковых целочисленных типов, я не могу возразить против неудобного переполнения при вычитании. Да, оно может вызывать всевозможные неприятные ошибки, начиная от переполнения буфера и заканчивая ошибками «out of memory». Переход на знаковые целые целесообразен, поскольку сильно отрицательное значение очевидно вскрывает факт ошибки. Также с такой позиции выступают многие, кто использует знаковое значение на все случаи жизни. Стыдно так делать, поскольку так вы теряете возможность выражать что-либо в системе типов.

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

Представляется, что это равноценно знаковому целому числу с более многочисленными вариантами НП, так стоит ли этим заниматься?

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

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

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

Различные битовые векторы в сравнении с целочисленным типом


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

Мне всегда казалось странным, что мы относимся к целочисленному типу как к числу и выполняем над ним арифметические действия, одновременно изменяя отдельные разряды. Когда вы занимаетесь математикой, вам редко приходится оперировать отдельными цифрами! Это особенно верно для знаковых целых чисел, где знаковый бит всё портит и приводит к определенному или неопределенному поведению при операциях сдвига.

Конечно, разделение этих двух операций делает написание таких оптимизаций, как shift вместо division или bitwise and вместо modulo, более назойливым, а некоторые причудливые битовые уловки требуют больше приведений. Но, в то же время, так становится очевиднее, что именно происходит: мы начинаем рассматривать целое число как последовательность битов. Так приобретается некоторый выигрыш в производительности, но это требуется дополнительно документировать.

Когда я упомянул оптимизацию сдвига, я не имел в виду замену x / 2 на x >> 1 — это сделает компилятор. Я говорю о таких вещах, как hash % hash_table_size, где hash_table_size всегда является степенью двойки, но компилятор не может этого знать.

В то время как мы решаем эту задачу: почему мы зарезервировали так много токенов для битовых операций? Как часто нам действительно нужны |, & или ~, и стоит ли затрачивать на них целый символ, который не может быть использован ни для чего другого? Не говоря уже о том, что они имеют неправильный приоритет в C, и сами по себе не очень полезны: вам часто нужны is_bit_set(x, n), extract_bits(x, low, high) или другие операции более высокого уровня, реализованные поверх битовых операций. Я бы хотел, чтобы эти операции были переданы (встроенным) функциям стандартной библиотеки, чтобы мы могли повторно использовать операторы для чего-то другого, например, для конвейеров.

Заключение


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

Симметричные знаковые целые числа упрощают работу со многими фундаментальными API, а 63-битный беззнаковый size_t может сочетать лучшее из знакового и беззнакового при работе с контейнерами. Конечно, нам пока не обойтись без настоящих беззнаковых типоа в ситуациях, когда требуется дополнительный бит. Но, поскольку он удваивает диапазон, я думаю, что было бы неплохо отказаться от настоящего INT_MIN, кроме как для взаимодействия с C. Различные битовые векторы могут повысить выразительность кода, но требуемые при этом преобразования также могут доставить немало головной боли. Я бы все же хотел, чтобы кто-нибудь попробовал такие эксперименты.

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


  1. NeoCode
    14.06.2023 13:21
    +1

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

    Идею с INT_NAN поддерживаю.

    Еще есть понятие обработки переполнения. Есть понятие режим насыщения, когда 255+1==255, а не 0, но не знаю в каких языках оно поддерживается. Зато в C# есть checked и unchecked. В общем, эти фундаментальные вещи хорошо бы иметь на уровне языка.

    И еще конечно числа неограниченной длины на уровне числовых литералов (а не строковых, как делается во всяких GMP).


    1. Goron_Dekar
      14.06.2023 13:21
      +2

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


      1. NeoCode
        14.06.2023 13:21
        +1

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


        1. Zuy
          14.06.2023 13:21

          Там где я видел их применение и сам пользовался, использовалась IQ либа от TI на их же контроллерах. Причем под целую часть все оставляли только знак. По сути при вычислениях они были всегда в диапазоне -1..+1. Ну а для перевода в/из реальных значений использовались коэффициенты масштабирования. вот эта штука сильно мозг ломала поначалу.


      1. SpiderEkb
        14.06.2023 13:21
        +1

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

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

        Правда, числа с фиксированной точкой не являются "обычными" целыми числами - формат хранения их отличается от обычного int. Существует два формата чисел с фиксированной точкой - packed и zoned:

        Представление числа 21544 в различных форматах
        Представление числа 21544 в различных форматах

        Для формата zoned есть еще один "сахарок" - для положительных чисел внутреннее представление zoned совпадает со строковым - в кодовой таблице EBCDIC, используемой в данной системе, цифры 0..9 имеют коды 0xF0..0xF9 т.е. строка "21544" в памяти представлена как F2F1F5F4F4 что полностью совпадает с ее zoned представлением.

        Для компилятора С есть расширение в виде макроса decimal():

        /* this example demonstrates the use of the decimal type */
        
        #include <decimal.h>
        
        decimal(31,4) pd01 = 1234.5678d;
        decimal(29,4) pd02 = 1234.5678d;
        
        int main(void)
        {
          /* The results are different in the next two statements */
          pd01 = pd01 + 1d;
          pd02 = pd02 + 1d;
        
          printf("pd01 = %D(31,4)\n", pd01);
          printf("pd02 = %D(29,4)\n", pd02);
        
          /* Warning: The decimal variable with size 31 should not be      */
          /*          used in arithmetic operation.                        */
          /*          In the above example: (31,4) + (1,0) ==> (31,3)      */
          /*                                (29,4) + (1,0) ==> (30,4)      */
        
          return(0);
        }

        В С++ определен шаблон _DecimalT<>

        #include<bcd.h>
        
        void main ()
        {
          _DecimalT<4,0> d40 = __D("123");              // OK
          _DecimalT<6,0> d60 = __D(d40);                // Because no constructor
                                                        // exists that can convert d40 to d60.
                                                        // macro __D is needed to convert d40
                                                        // into an intermediate type first.
                                                        
          d60 = d40;                                    // OK. This is different from the
                                                        // previous statement in which
                                                        // the constructor was called.
                                                        // In this case, the assignment
                                                        // operator is called and the
                                                        // compiler converts d40 into the
                                                        // intermediate type automatically.
                                                        
          _DecimalT<8,0> d80 = (_DecimalT<7,0>)1;       // OK
                                                        // Type casting an int,not a decimal(n,p)
                                                        
          _DecimalT<(9,0> d90;                          // OK
          
          d60 = (_DecimalT<7,0>)__D("12");              // OK
          
          d60 = (_DecimalT<4,0>)__D(d80);
          d60 = (_DecimalT<4,0>)__D(d80 + 1);           // In both cases, the resultant classes
                                                        // of the expressions are _DecimalT<n,p>.
                                                        // macro __D is needed to convert them
                                                        // to an intermediate type first.
                                                        
          d60 = (_DecimalT<4,0>)(d80 + (float)4.500);   // OK because the resultant type
                                                        // is a float
        }

        И то и другое работают с числами с фиксированной точкой в packed формате. Для формата zoned нет типов в С/С++

        Естественно, определена арифметика, преобразования типов.

        Тут надо иметь ввиду что если вы определили число с фиксированной точкой длиной 5 знаков и пытаетесь присвоить ему значение из >5 знаков:

        dcl-s var packed(5:0); // 5 символов, без запятой
        var = 123456;          // пытаемся занести в него болше чем влезет

        то получите системное исключение о переполнении.

        Также (в С/С++ нет, но в других языках да) определены операции присваивания с округлением - работают там, где количество знаков после запятой у lvalue меньше чем у rvalue

        dcl-s var1 packed(5:2);
        dcl-s var2 packed(5:3);
        
        var2 = 1.235;
        var1 = var2;         // var1 = 1.23
        eval(h) var1 = var2; // var1 = 1.24

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


        1. SIISII
          14.06.2023 13:21

          На мэйнфрейме z/Architecture, чтоль? Обработка десятичных чисел переменной длины -- фишка ещё с Системы 360...


          1. SpiderEkb
            14.06.2023 13:21

            Там тоже, вроде, есть, но это middleware IBM i (AS/400).

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

            Плюс packed и zoned соответствуют SQL-ным DECIMAL и NUMERIC


    1. domix32
      14.06.2023 13:21
      +1

      Вот да, чего не хватает так это decimal и bigint


      1. SpiderEkb
        14.06.2023 13:21

        Удобно еще универсальные нетипизированные определения типа *loval / *hival - минимальное/максимальное значение для данного типа. Т.е.

        double d = *loval;
        int i = *hival;
        char c = *loval;

        а компилятор уже сам нужное значение подставит...

        Пример использования (в другом языке):

        dcl-s result packed(15: 2);
        dcl-s uplimit packed(15:2) inz(*hival);
        dcl-s lowlimit packed(15:2) inz(*loval);
        dcl-s interRslt packed(63:2);
        
        // что-то вычисляем, результат в interRslt и потом
        if interRslt in %range(lowlimit: uplimit);
          // присвоение безопасно - переполлнения не будет
          result = interRslt;
        else;
          // фиксируем ошибку выхода за пределы возможных значений
        endif;

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


        1. domix32
          14.06.2023 13:21

          универсальные нетипизированные определения типа

          для такого есть std::numeric_limits из <limits> с его min, max, epsilon и прочими. Правда автовывод пока для них не работает, так что приходится явно указывать тип.


          1. 0xd34df00d
            14.06.2023 13:21

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


            1. domix32
              14.06.2023 13:21

              Можно сделать грязь.

              #include <limits>
              #include <iostream>
              
              using namespace std;
              template<typename T>
              constexpr void tmin(T& v) {
                  v = numeric_limits<T>::min();
              }
              int main() {
                  float f;  tmin(f);
                  int i;  tmin(i);
                  double d;  tmin(d);
                  cout <<i << " " << f << " " << d << endl;
                  return 0;
              }

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


              1. 0xd34df00d
                14.06.2023 13:21
                +1

                struct Min
                {
                  template<typename T>
                  operator T() { return std::numeric_limits<T>::min(); }
                };


          1. SpiderEkb
            14.06.2023 13:21

            Я в курсе что есть. А в С все это #define определено для каждого типа...

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


            1. SpiderEkb
              14.06.2023 13:21

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

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

              Есть оператор clear - сброс значения переменной к дефолтному для денного типа значению. Есть reset - сброс значения переменной к значению которым она была инициализирована при объявлении (если не было явной инициализации, то reset == clear).

              Со структурами очень гибко - можно явно размер указать (больше чем суммарный размер полей) - иногда бывает нужно. Можно явно инициализировать каждое поле по-отдельности. Можно явно указывать смещение поля внутри структуры (в т.ч. и перекрывать другие поля). Можно задавать неименованные поля (обозначаются *n) вместо всяких dummy, tmp. Например, шаблон для времени может выглядеть так:

              dcl-ds dsTimeTemplate;
                Hours   char(2);
                *n      char(1) inz(':');
                Minutes char(2);
                *n      char(1) inz(':');
                Seconds char(2);
                Time    char(8) samepos(Hours);
              end-ds;

              Time объявлено как перекрывающее все остальные поля.

              Разделители ':' объявлены как неименованные.

              Заполняем Hours, Minutes, Seconds и получаем в Time готовую строку с разделителями.

              Когда возвращаешься в С/С++ многих таких веще не хватает иногда. Т.е. там все это можно сделать, но нужно делать - класс прописывать, конструктор ему писать...

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


              1. nin-jin
                14.06.2023 13:21
                +1

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


                1. SpiderEkb
                  14.06.2023 13:21

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

                  В том и суть что очень много делается на этапе компиляции и относительно немного на этапе выполнения.


                  1. nin-jin
                    14.06.2023 13:21

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


                    1. SpiderEkb
                      14.06.2023 13:21

                      Никакого копирования.

                      Я объявляю

                      dcl-ds t_dsTimeTemplate qualified template;
                        Hours   char(2);
                        *n      char(1) inz(':');
                        Minutes char(2);
                        *n      char(1) inz(':');
                        Seconds char(2);
                        Time    char(8) samepos(Hours);
                      end-ds;

                      Ближайший аналог template - typedef в С. Т.е. описываю "тип" структуры.

                      Далее, где мне нужно уже объявляю переменную

                      dcl-ds dsTimeTemplate likeds(t_dsTimeTemplate) inz(*likeds);

                      Объявление переменной по шаблону с указанием правила ее инициализации "как прописано в шаблоне" - именованные поля инициализируются дефолтным для типа char значением (пробел), неименованные (разделители) - указанным для них значением ':'

                      И тут на этапе компиляции в памяти создается переменная dsTimeTemplate содержащая ' : : ' дальше остается просто заполнить поля нужными значениями

                      dsTimeTemplate.Hours   = '02';
                      dsTimeTemplate.Minutes = '13';
                      dsTimeTemplate.Seconds = '39';

                      И в dsTimeTemplate.Time получим '02:13:39'

                      Никаких "склеек строк", ничего. Просто присвоение (memcpy три раза по 2 символа). Разделители в нужных позициях расставлены на этапе компиляции. Для обращения ко всей строке целиком есть отдельное поле (аналог union в С, но более гибкий и наглядный).

                      Все это работает в обе стороны.

                      Например, есть тут такой тип - timestamp. Фактически - строка формата YYYY.MM.DD.HH:MM:SS.ssssss Можно для нее прописать аналогичный шаблон,

                      dcl-ds dsTimeStamp;
                        Year char(4);
                        *n char(1) inz('.');
                        Month char(2);
                        *n char(1) inz('.');
                        Day char(2);
                        *n char(1) inz('.');
                        Hour char(2);
                        *n char(1) inz(':');
                        Minute char(2);
                        *n char(1) inz(':');  
                        Second char(2);
                        *n char(1) inz('.');
                        mSecond char(6);
                        TS char(26) samepos(Year);
                      end-ds;

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


                      1. nin-jin
                        14.06.2023 13:21

                        То есть у вас сначала идёт ещё и двойная инициализация полей.

                        При парсинге надо ситаксис чекать, а не просто на фиксированные подстроки разбивать. Да и на выходе надо числа иметь, а не просто подстроки.


                      1. SpiderEkb
                        14.06.2023 13:21

                        Что именно и где надо - это от задачи зависит.

                        Где надо синтаксис - там есть свои инструменты. Где надо числа - там свои. Это просто в качестве наглядного примера.


                      1. nin-jin
                        14.06.2023 13:21

                        Ну так расскажите, что это за задачи такие, где строка "12345678" должна парситься как 12 часов 45 минут и 78 секунд.


                      1. SpiderEkb
                        14.06.2023 13:21

                        Вы валидацию с парсингом не путаете? Или для вас это одно и то же?

                        Для валидации есть

                        test(de) *cymd var;
                        if %error;
                          // Это ерунда какая-то, но точно не дата в формате *CYMD
                        endif;
                        
                        test(te) *hms var;
                        if %error;
                          // Это не время в формате *HMS
                        endif;

                        Причем, var может быть как строкой, так и числом.

                        Если все ок, то дальше можно или втащить в шаблон и оттуда вытащить по частям (нужно число - ну сделайте %int(dsTimeTemplate.Hours))

                        А можно использовать встроенные

                        %SUBDT(value:*MSECONDS|*SECONDS|*MINUTES|*HOURS|*DAYS|*MONTHS|*YEARS)
                        %SUBDT(value:*MS|*S|*MN|*H|*D|*M|*Y)

                        Бывают ситуации когда приходит дата, но вы не знаете в каком она формате - может так, а может этак...

                        test(de) *eur var;
                        if %error;
                          test(de) *iso var;
                          if %error;
                            // это вообще непойми что
                          else;
                            // Дата в формате *iso
                          endif;
                        else;
                          // Это дата в формате *eur
                        endif;

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

                        И да. Есть вещи, которые я на С реализую быстрее. Скажем, преобразование формата MITIME (это такая феерическая хрень - количество микросекунд от начала эпохи, середина которой приходится на 2000.01.01.00:00:00.000000) в строку timestamp. Но тут просто потому что системное API слишком универсально чтобы работать быстро.

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


                      1. nin-jin
                        14.06.2023 13:21

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


                      1. SpiderEkb
                        14.06.2023 13:21

                        test(de) - встроенная валидация даты (d) с фиксацией ошибки (e - без e будет кидаться системное исключение, с e устанавливается флаг %error подробности можно посмотреть в переменной %status).

                        Опирается оно на какие-то там системные потроха. И работает со всеми мыслимыми форматами:

                        *EUR - 16.05.2023

                        *ISO - 2023-05-16

                        *CYMD - 1230516

                        и еще много чего там может быть. Отвечает на вопрос "является ли это датой в данном формате".

                        Т.е. строка '2023-05-16' или число 20230516 будут валидны для формата *ISO

                        Строка '16.05.2023' или число 16052023 валидны в формате *EUR

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


                      1. nin-jin
                        14.06.2023 13:21

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


                      1. SpiderEkb
                        14.06.2023 13:21

                        Хорошо. Не зная формата:

                        10121012 - это 1012-10-12 (12 октября 1012-го года) в *ISO или 10-12-1012 (10 декабря 1012-го года) в *EUR?

                        Можно и позаковыристее придумать пример, но лень.

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

                        Валидация и разбивка на компоненты - разные операции. Мешать их в кучу -ошибка (увы, типичная ошибка).


                      1. nin-jin
                        14.06.2023 13:21

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


                      1. SpiderEkb
                        14.06.2023 13:21

                        И чтобы процессору работать с этими вашими "zoned числами", компилятору приходится вставлять парсинг чисел на лету, что не очень эффективно

                        Не совсем понял о чем вы?

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

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

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

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

                        И опять ДА - я не сравниваю языки какой лучше или хуже. Я сравниваю отдельные возможности. Чего-то мне не хватает в том языке на котором больше пишу сейчас (последние 6 лет), чего-то не хватает в С/С++ (на которых я и сейчас иногда пишу, а до этого писал лет 25...).


                      1. SpiderEkb
                        14.06.2023 13:21

                        Ну вот пример. Есть дата (она уже провалидирована ранее) в формате *CYMD это 7-значный формат для хранения дат в диапазоне 1900-01-01.00:00:00 - 2999-12-31.23:59:59.

                        Выглядит как CYYMMDD где C = 0 для дат 20-го века, 1 для дат 21-го века.

                        Т.е. для 17-го июня 2023 года это будет число 1230617

                        Традиционно у нас такие даты хранятся в переменной типа zoned(7:0) (что такое zoned внутри тут приводил картинку).

                        И вот из этой даты нужно извлечь год.

                        По типам данных

                        ind (индикатор) - логический тип данных. Фактически - char(1) у которого возможны два значения - '1' - *on и '0' - *off

                        uns(3) - unsigned char - беззнаковое однобайтовое целое

                        uns(5) - unsigned short - беззнаковое двухбайтовое целое

                        Функция:

                        dcl-proc getCYMDYear;
                          dcl-pi *n int(5);
                            zDate zoned(7: 0) value;
                          end-pi;
                        
                          dcl-ds dsZonedDate qualified;
                            zDate   zoned(7: 0) inz;
                            cent21  ind         pos(1);
                            yearHi  uns(3)      pos(2);
                            yearLo  uns(3)      pos(3);
                          end-ds;
                          
                          dcl-s mask  int(3)  inz(15);
                          dcl-s year  int(5);
                        
                          dsZonedDate.zDate = zDate;
                        
                          // Используем особенности формата zoned
                          // дата 17 Июня 2023 (17-06-2023) будет представлена как
                          // 1230617 а в памяти лежать в виде F1F2F3F0F6F1F7
                          // Надо взять второй байт, сделать побитовое И с маской 0F,
                          // умножить на 10 и прибавить результат побитового И третьего
                          // байта с маской 0F
                          year = %bitand(dsZonedDate.yearHi: mask) * 10 
                               + %bitand(dsZonedDate.yearLo: mask);
                        
                          // Добавляем столетие
                          // Если первый символ '0' (*off) - 20-й век
                          // Если '1' (*on) - 21-й
                          if dsDate.cent21;
                            year += 2000;
                          else;
                            year += 1900;
                          endif;
                        
                          return year;
                        end-proc;

                        Итого: два побитовых И, одной умножение, два сложения. Все.

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

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

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

                        Почему сейчас нельзя это реализовать с С++?


                      1. domix32
                        14.06.2023 13:21

                        Скорее вы описываете union типы. Правда в случае с С++ чтобы написать ровно такое же придётся написать заметно больше кода. Ну и там будут не явные поля, а методы с вьюхами. Не думаю, что в языке появится что-то совсем аналогичное, т.к. оно не слишком general purpose. Теоретически в 20+ стандартах можно попробовать что-то по аналогии с format сделать.


                      1. SpiderEkb
                        14.06.2023 13:21

                        Ну да, типа union, но гибче. Здесь структура (ds) рассматривается как некий массив байт в котором можно для каждого поля (независимо от остальных) указывать его положение в структуре и тип. Никто не запрещает перекрытия полей.

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

                        dcl-ds dsStrZoned;
                          zField zoned(7:0);
                          strField char(7) pos(1); // или samepos(zField)
                        end-ds;

                        при занесении туда строки dsStrZoned.strField = '1234567' в числовом поле dsStrZoned.zField окажется число с фиксированной точкой 1234567.

                        Такой вот простой и понятный механизм. И очень универсальный.

                        Из практического примера. 20-значный банковский счет. Первые 5 цифр - тип счета (иногда называют "счет второго порядка"). Затем три цифры - код валюты счета. Пишем:

                        dcl-ds dsAccount;
                          accNo    char(20);
                          accType  char(5) pos(1);
                          currCode char(3) pos(6);
                        end-ds;

                        Присваиваем номер счета dsAccount.accNo и автоматом получаем отдельно его 5-значный тип в dsAccount.accType и код валюты счета в dsAccount.currCode без каких-либо затрат на извлечение составляющих.

                        Да, в С все это тоже несложно через указатели на смещение относительно начала структуры. Но не так прозрачно.


              1. eao197
                14.06.2023 13:21

                И уж есть С++ неуклонно движется в сторону более высокоуровнего языка

                Но это не точно.


                1. SpiderEkb
                  14.06.2023 13:21

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

                  Насколько получается... Ну не знаю. Я не сильно за новшествами слежу т.к. с 17-го года С/С++ для меня "второй язык", не основной. И пишу на нем далеко не каждый день. И даже больше на С или "С с классами" чем на полноценном С++ т.к. тут речь не о сложной логике - для этого есть более удобный язык, но о всяких достаточно низкоуровневых вещах, плотном взаимодействии с системой.


                  1. eao197
                    14.06.2023 13:21

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

                    Так оно и есть. Но на какой-то принципиально новый уровень высокоуровневости языка пока так и не вышел. Возможно, когда в C++ завезут рефлексию, тогда в каких-то моментах станет сильно лучше, но все равно от заботы за тем, чтобы память не утекала, ссылки не протухали, а исключения не вылетали из noexcept-контекстов, останутся на программисте. Не говоря уже про россыпь UB :)


        1. nin-jin
          14.06.2023 13:21

          Если я правильно понял, что вы тут делаете, то на высокоуровневом языке это выглядит как-то так:

          short result;
          int interResult = 1234567;
          
          try {
            // что-то вычисляем, результат в interRslt и потом
            result = interResult.to!short;
          } catch( ConvOverflowException error ) {
            // фиксируем ошибку выхода за пределы возможных значений
          }


  1. MasterMentor
    14.06.2023 13:21
    +2

    Я, конечно, приветствую ещё одну статью, из разряда много-численных, исследующих "реализацию целых чисел в различных ЯП-ах". Так повелось, но эти реализации отличаются друг от друга лишь количеством бит (байт), выделенных под целое число. Длинной арифметики ( см. https://ru.wikipedia.org/wiki/Длинная_арифметика ) и даже поиска в Google ( см. https://www.google.com/search?q=целые+числа+произвольной+длины ) чтобы раз и навсегда закрыть тему "в такой фундаментальной области, как работа с целочисленными типами", явно недостаточно. Необходимо требуются инновации, и, несомненно, освоение государственных грантов: на глубокое исследование темы, с привлечением ИИ и "глубоких нейросетей", и, возможно, блокчейна с NFT-токенами. В общем, есть куда развиваться.

    А вот это, как мне кажется, ноу-хау и даже тема для диссертации:

    "Итак, вот чего я хочу: знаковое целое число, где INT_MIN == -INT_MAX, полученное путем перемещения минимального значения на единицу вверх.

    Во-первых, вы не теряете ничего полезного: Я бы сказал, что дополнительное отрицательное число не имеет значения в большинстве случаев. В конце концов, если бы у вас было лишнее число, разве не логичнее было бы иметь дополнительное положительное число вместо отрицательного?"


  1. SIISII
    14.06.2023 13:21
    +4

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


  1. leshabirukov
    14.06.2023 13:21
    +5

    Автора бить Кнутом. Всеми четырьмя томами по голове, (еще Генри Уоррена добавить для надёжности, жаль тонковат).

    Фишка С\С++ в хорошем соответствии ассемблеру, то есть системе команд, то есть архитектуре. Причем не x86 или ARM скажем, а всем им сразу, в том числе и там где памяти и мощей с гулькин нос, но давайте вот это всё эмулировать.


    1. Varias_Sferd
      14.06.2023 13:21

      При чем здесь Кнут? Предложенные автором типы никак не уменьшают производительность. Даже больше, такой тип, это дополнительная информация о типе. Для оптимизирующего компилятора такой тип, если он является встроенным - это громадное пространство для оптимизаций, в том числе и упаковка std::optional в sizeof(int).

      Ну и C и C++ - это языки высокого уровня. Ни больше ни меньше. Соответствие ассемблеру вещь весьма растяжимая. Пространство для compile time оптимизаций у C++ не такое большое, как принято считать. Да, Java или C# безнадежно хуже из-за ссылочных типов данных или своей модели памяти. Да и вообще, собрать их "без рантайма" ещё нужно постараться.

      Но это не значит, что C и C++ идеальны или достаточно хороши в этом плане. Чтобы "ускорить" эти языки добавили дыры (UB) в стандарт, чтобы у компилятора было больше информации. Например, числовые знаковые числа в языках C и C++ довольно абстрактны. В стандарте не определено, что будет происходить при переполнении. Может происходить вообще всё что угодно.

      О каком соответствии ассемблеру идёт речь? Различные системы команд могут разительно отличаться. Регистр с флагом переполнения может быть, а может и не быть. Может использоваться совсем другая модель памяти с несколькими адресными пространствами.

      Там где мощностей мало, без расширений семантики С или C++ достаточно сложно писать. Иначе, когда тебе нужно сделать какую-то специфичную для процессора вещь, ты просто теряешься в догадках, а не сломается ли твой код из-за UB, когда тебе нужно потрогать стек поинтер.


      1. leshabirukov
        14.06.2023 13:21

        Добро пожаловать на Хабр.
        Кнут при том, что понимает и алгоритмы, и архитектуру. Дополнительный код занял доминирующее положение не по чьему-то произволу, а потому, что это на практике лучший способ проецирования целых чисел на битовые строки. За подробностями - в "Исскуство программирования", или в "Hackers Delight" Уорена.


    1. domix32
      14.06.2023 13:21

      в хорошем соответствии ассемблеру

      Лол, а какому ассемблеру? PDP? VAX? MIPS? ARM? Intel? m68k? Си изобретался именно для того чтобы можно было отдалиться от ассемблера и писать универсальный код, который впоследствии превращается в конкретный ассемблер для конкретной архитектуры и всё соотвествие находится в компиляторах, а не в языке.


      1. leshabirukov
        14.06.2023 13:21
        +1

        По-моему из перечисленного вами, всё кроме самых ранних PDP использует дополнительный код.


  1. vadimr
    14.06.2023 13:21
    +4

    Ненормализованные вещественные числа обеспечивают бо́льшую часть того, что он описал. В старинных машинах вроде CDC-6600 и БЭСМ-6 их фактически и использовали вместо целых. Потом почему-то от этой практики отказались, автору неплохо бы изучить причины. IBM озолотилась, когда вместо вот этого всего придумала современную систему с униформными битами и байтами.

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


  1. IsKaropki
    14.06.2023 13:21
    +5

    >Во-вторых, вы восстанавливаете симметрию.

    - Доктор, у меня одно яичко чуть выше другого.

    - Ну и что тут такого?

    - Как что? Неаккуратно как-то.


  1. nin-jin
    14.06.2023 13:21

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


  1. IsKaropki
    14.06.2023 13:21

    Вспомнилось: в советской ЭВМ Д3-28, в одном из вариантов представления чисел, для знака был выделен отдельный бит (или два - для мантиссы и порядка чисел с плавающей запятой). Там была "симметрия". Отрицательные числа выглядели точно так же, как и положительные (если не учитывать знаковый бит). Правда, в итоге, нулей было тоже два: минус ноль, и плюс ноль, в зависимости от состояния знакового бита. Ну и, в итоге, был некоторый беспорядок в операциях сравнения с нулём: иногда знак нуля учитывался, иногда нет.


  1. flashmozzg
    14.06.2023 13:21
    +2

    Зашёл ожидая увидеть предложения о нормальных (не тайпдефах, которые везде разные и от того из не поиспольщуешь в публичном api) типах фиксированной длины, без неявного преобразования (которые уже есть в кланге в виде ExactInt, емнип, и которые закрывают п.2 статьи), а увидел что-то странное, косвенно свидетельствующее о том, что автор не понимает вообще как работают процессоры, компиляторы и язык C++.

    Да, давайте превратим поле целых чисел в непонятно что, что ни в мапу не положишь, ни отсортируешь (неговоря уже об эффективности всего этого дела), лишь для "симметричности яичек" xD

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


  1. BraveBanana
    14.06.2023 13:21

    Если это сделать, придется выбросить весь ленаси код и обратную совместимость. Забудьте, никто никогда на это не пойдёт