Коллекция примеров 64-битных ошибок в реальных программах — часть 2

    << Читать первую часть статьи



    Пример 16. Адресная арифметика. A + B != A — (-B)


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

    Большой процент 64-битных ошибок связан именно с адресной арифметикой. Часто ошибки возникают в тех выражениях, где совместно используются указатели и 32-битные переменные.

    Рассмотрим первую из ошибок данного типа:

    char *A = "123456789";
    unsigned B = 1;
    char *X = A + B;
    char *Y = A - (-B);
    if (X != Y)
      cout << "Error" << endl;

    Причина, по которой в Win32 программе A + B == A — (-B), показана на рисунке 14.

    Picture 14
    Рисунок 14 — Win32: A + B == A — (-B)

    Причина, по которой в Win64 программе A + B != A — (-B), показана на рисунке 15.

    Picture 15
    Рисунок 15 — Win64: A + B != A — (-B)

    Ошибка будет устранена, если использовать подходящий memsize-тип. В данном случае используется тип ptrdfiff_t:

    char *A = "123456789";
    ptrdiff_t B = 1;
    char *X = A + B;
    char *Y = A - (-B);

    Пример 17. Адресная арифметика. Знаковые и беззнаковые типы.


    Рассмотрим еще один вариант ошибки, связанный с использованием знаковых и беззнаковых типов. В этот раз ошибка приведет не к неверному сравнению, а сразу к падению приложения.
    LONG p1[100];
    ULONG x = 5;
    LONG y = -1;
    LONG *p2 = p1 + 50;
    p2 = p2 + x * y;
    *p2 = 1; // Access violation

    Выражение «x * y» имеет значение 0xFFFFFFFB и имеет тип unsigned. Данный код, собранный в 32-битном варианте работоспособен, так как сложение указателя с 0xFFFFFFFB эквивалентно его уменьшению на 5. В 64-битной указатель после прибавления 0xFFFFFFFB начнет указывать далеко за пределы массива p1 (смотри рисунок 16).

    Picture 16

    Рисунок 16 — Выход за границы массива

    Исправление заключается в использовании memsize-типов и аккуратной работе со знаковыми и беззнаковыми типами:

    LONG p1[100];
    LONG_PTR x = 5;
    LONG_PTR y = -1;
    LONG *p2 = p1 + 50;
    p2 = p2 + x * y;
    *p2 = 1; // OK

    Пример 18. Адресная арифметика. Переполнения.

    class Region {
      float *array;
      int Width, Height, Depth;
      float Region::GetCell(int x, int y, int z) const;
      ...
    };
    
    float Region::GetCell(int x, int y, int z) const {
      return array[x + y * Width + z * Width * Height];
    }

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

    Приведенный код корректно работает с указателями, если значение выражения " x + y * Width + z * Width * Height" не превышает INT_MAX (2147483647). В противном случае произойдет переполнение, что приведет к неопределенному поведению в программы.

    Такой код мог всегда корректно работать на 32-битной платформе. В рамках 32-битной архитектуры программе недоступен объем памяти для создания массива подобного размеров. На 64-битной архитектуре это ограничение снято, и размер массива легко может превысить INT_MAX элементов.

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

    float Region::GetCell(int x, int y, int z) const {
    return array[static_cast<ptrdiff_t>(x) + y * Width +
    z * Width * Height];
    }

    Они знают, что по правилам языка Си++ выражение для вычисления индекса будет иметь тип ptrdiff_t и надеются за счет этого избежать переполнения. Но переполнение может произойти внутри подвыражения «y * Width» или «z * Width * Height», так как для их вычисления по-прежнему используется тип int.

    Если вы хотите исправить код, не изменяя типов переменных, участвующих в выражении, то вы можете явно привести каждое подвыражение к типу ptrdiff_t:

    float Region::GetCell(int x, int y, int z) const {
      return array[ptrdiff_t(x) +
                   ptrdiff_t(y) * Width +
                   ptrdiff_t(z) * Width * Height];
    }

    Другое, более верное решение — изменить типы переменных:

    typedef ptrdiff_t TCoord;
    class Region {
      float *array;
      TCoord Width, Height, Depth;
      float Region::GetCell(TCoord x, TCoord y, TCoord z) const;
      ...
    };
    
    float Region::GetCell(TCoord x, TCoord y, TCoord z) const {
      return array[x + y * Width + z * Width * Height];
    }

    Пример 19. Изменение типа массива


    Иногда в программах для удобства изменяют тип массива при его обработке. Опасное и безопасное приведение типов представлено в следующем коде:

    int array[4] = { 1, 2, 3, 4 };
    enum ENumbers { ZERO, ONE, TWO, THREE, FOUR };
    
    //safe cast (for MSVC)
    ENumbers *enumPtr = (ENumbers *)(array);
    cout << enumPtr[1] << " ";
    
    //unsafe cast
    size_t *sizetPtr = (size_t *)(array);
    cout << sizetPtr[1] << endl;
    
    //Output on 32-bit system: 2 2
    //Output on 64-bit system: 2 17179869187

    Как видите, результат вывода программы отличается в 32-битном и 64-битном варианте. На 32-битной системе доступ к элементам массива осуществляется корректно, так как размеры типов size_t и int совпадают, и мы видим вывод «2 2».

    На 64-битной системе мы получили в выводе «2 17179869187», так как именно значение 17179869187 находится в 1-ом элементе массива sizetPtr (см. рисунок 17). В некоторых случаях именно такое поведение и бывает нужно, но обычно это является ошибкой.

    Picture 17

    Рисунок 17 — Представление элементов массивов в памяти

    Примечание. Тип enum в компиляторе Visual C++ по умолчанию совпадает размером с типом int, то есть является 32-битным типом. Использование enum другого размера возможно только с помощью расширения, считающимся нестандартным в Visual C++. Поэтому приведенный пример корректен в Visual C++, но с точки зрения других компиляторов приведение указателя на элементы int к указателю на элементы enum может быть также некорректным.

    Пример 20. Упаковка указателя в 32-битный тип


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

    char *ptr = ...;
    int n = (int) ptr;
    ...
    ptr = (char *) n;

    В 64-битной программе это некорректно, поскольку тип int остался 32-битным и не может хранить в себе 64-битный указатель. Часто это не удается заметить сразу. Благодаря стечению обстоятельств при тестировании указатель может всегда ссылаться на объекты, расположенные в младших 4 гигабайтах адресного пространства. В этом случае 64-битная программа будет удачно работать, и может неожиданно отказать только спустя большой промежуток времени (см. рисунок 18).

    Picture 18

    Рисунок 18 — Помещение указателя в переменную типа int

    Если все же необходимо поместить указатель в переменную целочисленного типа, то следует использовать такие типы как intptr_t, uintptr_t, ptrdiff_t и size_t.

    Пример 21. Memsize-типы в объединениях


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

    union PtrNumUnion {
      char *m_p;
      unsigned m_n;
    } u;
    
    u.m_p = str;
    u.m_n += delta;

    Данный код корректен на 32-битных системах и некорректен на 64-битных. Изменяя член m_n на 64-битной системе, мы работаем только с частью указателя m_p (смотри рисунок 19).

    Picture 19
    Рисунок 19 — Представление объединения в памяти на 32-битной и 64-битной системе.

    Следует использовать тип, который будет соответствовать размеру указателя:

    union PtrNumUnion {
      char *m_p;
      uintptr_t m_n; //type fixed
    } u;

    Пример 22. Вечный цикл


    Смешанное использование 32-битных и 64-битных типов неожиданно может привести к возникновению вечных циклов. Рассмотрим синтетический пример, иллюстрирующий целый класс подобных дефектов:

    size_t Count = BigValue;
    for (unsigned Index = 0; Index != Count; Index++)
    { ... }  

    Это цикл никогда не прекратится, если значение Count > UINT_MAX. Предположим, что на 32-битных системах этот код работал с количеством итераций менее значения UINT_MAX. Но 64-битный вариант программы может обрабатывать больше данных, и ему может потребоваться большее количество итераций. Поскольку значения переменной Index лежат в диапазоне [0..UINT_MAX], то условие «Index != Count» никогда не выполнится, что и приводит к бесконечному циклу (см. рисунок 20).

    Picture 20

    Рисунок 20 — Механизм возникновения вечного цикла

    Пример 23. Работа с битами и операция NOT


    Работа с битовыми операциями требует особой аккуратности от программиста при разработке кроссплатформенных приложений, в которых типы данных могут иметь различные размеры. Поскольку перенос программы на 64-битную платформу также ведет к изменению размерности некоторых типов, то высока вероятность возникновения ошибок в участках кода, работающих с отдельными битами. Чаще всего это происходит из-за смешенной работы с 32-битными и 64-битными типами данных. Рассмотрим ошибку, возникшую в коде из-за некорректного применения операции NOT:

    UINT_PTR a = ~UINT_PTR(0);
    ULONG b = 0x10;
    UINT_PTR c = a & ~(b - 1);
    c = c | 0xFu;
    if (a != c)
      cout << "Error" << endl;

    Ошибка заключается в том, что маска, заданная выражением "~(b — 1)", имеет тип ULONG. Это приводит к обнулению старших разрядов переменной «a», ходя должны были обнулиться только младшие четыре бита (см. рисунок 21).

    Picture 21

    Рисунок 21 — Ошибка из-за обнуления старших бит

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

    UINT_PTR c = a & ~(UINT_PTR(b) - 1);

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

    Пример 24. Работа с битами, сдвиги


    ptrdiff_t SetBitN(ptrdiff_t value, unsigned bitNum) {
      ptrdiff_t mask = 1 << bitNum;
      return value | mask;
    }

    Приведенный код работоспособен на 32-битной архитектуре и позволяет выставлять бит с номерами от 0 до 31 в единицу. После переноса программы на 64-битную платформу возникает необходимость выставлять биты с номерами от 0 до 63. Однако данный код неспособен выставить старшие биты, с номерами 32-63. Обратите внимание, что числовой литерал «1» имеет тип int, и при сдвиге на 32 позиции произойдет переполнение, как показано на рисунке 22. Получим мы в результате 0 (рисунок 22-B) или 1 (рисунок 22-C) — зависит от реализации компилятора.

    Picture 45

    Рисунок 22 — a) корректная установка 31-ого бита в 32-битном коде (биты считаются от 0); b,c) — Ошибка установки 32-ого бита на 64-битной системе (два варианта поведения, зависящих от компилятора)

    Для исправления кода необходимо сделать константу «1» того же типа, что и переменная mask:

    ptrdiff_t mask = static_cast<ptrdiff_t>(1) << bitNum;

    Заметим также, что неисправленный код приведет еще к одной интересной ошибке. При выставлении 31 бита на 64-битной системе результатом работы функции будет значение 0xffffffff80000000 (см. рисунок 23). Результатом выражения 1 << 31 является отрицательное число -2147483648. Это число представляется в 64-битной целой переменной как 0xffffffff80000000.

    Picture 49

    Рисунок 23 — Ошибка установки 31-ого бита на 64-битной системе

    Пример 25. Работа с битами и знаковое расширение


    Приведенная далее ошибка редка, но, к сожалению, достаточно сложна в понимании. Поэтому остановимся на ней чуть подробнее.
    struct BitFieldStruct {
      unsigned short a:15;
      unsigned short b:13;
    };
    
    BitFieldStruct obj;
    obj.a = 0x4000;
    size_t x = obj.a << 17; //Sign Extension
    printf("x 0x%Ix\n", x);
    //Output on 32-bit system: 0x80000000
    //Output on 64-bit system: 0xffffffff80000000

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

    Picture 24
    Рисунок 24 — Вычисление выражения «obj.a << 17» в 32-битном коде

    Обратим внимание, что при вычислении выражения «obj.a << 17» происходит знаковое расширение типа unsigned short до типа int. Более наглядно, это может продемонстрировать следующий код:

    #include <stdio.h>
    
    template <typename T> void PrintType(T)
    {
      printf("type is %s %d-bit\n",
              (T)-1 < 0 ? "signed" : "unsigned", sizeof(T)*8);
    }
    
    struct BitFieldStruct {
      unsigned short a:15;
      unsigned short b:13;
    };
    
    int main(void)
    {
      BitFieldStruct bf;
      PrintType( bf.a );
      PrintType( bf.a << 2);
      return 0;
    }
    
    Result:
    type is unsigned 16-bit
    type is signed 32-bit

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

    Picture 25
    Рисунок 25 — Вычисление выражения «obj.a << 17» в 64-битном коде

    Член структуры obj.a преобразуется из битового поля типа unsigned short в int. Выражение «obj.a << 17» имеет тип int, но оно преобразуется в ptrdiff_t и затем в size_t, перед тем как будет присвоено переменной addr. В результате мы получим число значение 0xffffffff80000000, вместо ожидаемого значения 0x0000000080000000.

    Будьте внимательны при работе с битовыми полями. Для предотвращения описанной ситуации в нашем примере достаточно явно привести obj.a к типу size_t.

    ...
    size_t x = static_cast<size_t>(obj.a) << 17; // OK
    printf("x 0x%Ix\n", x);
    //Output on 32-bit system: 0x80000000
    //Output on 64-bit system: 0x80000000

    Пример 26. Сериализация и обмен данными


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

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

    size_t PixelsCount;
    fread(&PixelsCount, sizeof(PixelsCount), 1, inFile);

    Недопустимо использование типов, которые меняют свой размер в зависимости от среды разработки, в бинарных интерфейсах обмена данными. В языке Си++ большинство типов не имеют четкого размера и, следовательно, их все невозможно использовать для этих целей. Поэтому создатели средств разработки и сами программисты создают типы данных, имеющие строгий размер, такие как __int8, __int16, INT32, word64 и так далее.

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

    Порядок байт — метод записи байтов многобайтовых чисел (см. рисунок 26). Порядок от младшего к старшему (англ. little-endian) — запись начинается с младшего и заканчивается старшим. Этот порядок записи принят в памяти персональных компьютеров с x86 и x86-64-процессорами. Порядок от старшего к младшему (англ. big-endian) — запись начинается со старшего и заканчивается младшим. Этот порядок является стандартным для протоколов TCP/IP. Поэтому, порядок байтов от старшего к младшему часто называют сетевым порядком байтов (англ. network byte order). Этот порядок байт используется процессорами Motorola 68000, SPARC.
    Кстати, некоторые процессоры могут работать и в порядке от младшего к старшему, и наоборот. К их числу относится, например IA-64.

    Picture 51

    Рисунок 26 — Порядок байт в 64-битном типе на little-endian и big-endian системах

    Разрабатывая бинарный интерфейс или формат данных, следует помнить о последовательности байт. А если 64-битная система, на которую Вы переносите 32-битное приложение, имеет иную последовательность байт, то вы просто будете вынуждены учесть это в своем коде. Для преобразования между сетевым порядком байт (big-endian) и порядком байт (little-endian), можно использовать функции htonl(), htons(), bswap_64, и так далее.

    Пример 27. Изменение выравнивания типов


    Помимо изменения размеров некоторых типов данных, ошибки могут возникать и из-за изменения правил их выравнивания в 64-битной системе (см. рисунок 27).

    Picture 27

    Рисунок 27 — Размеры типы и границы их выравнивания (значения точны для Win32/Win64, но могут варьироваться в «Unix-мире» и приводятся просто в качестве примера)

    Рассмотрим пример описания проблемы, найденного в одном из форумов:

    Столкнулся сегодня с одной проблемой в Linux. Есть структура данных, состоящая из нескольких полей: 64-битный double, потом 8 unsigned char и один 32-битный int. Итого получается 20 байт (8 + 8*1 + 4). Под 32-битными системами sizeof равен 20 и всё работает нормально. А под 64-битным Linux'ом sizeof возвращает 24. Т.е. идёт выравнивание по границе 64 бит.

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

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

    Picture 28

    Рисунок 28 — Схематическое изображение структур и правил выравнивания типов

    Пример 28. Выравнивания типов и почему нельзя писать sizeof(x) + sizeof(y)


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

    struct MyPointersArray {
      DWORD m_n;
      PVOID m_arr[1];
    } object;
    ...
    malloc( sizeof(DWORD) + 5 * sizeof(PVOID) );
    ...

    Этот код будет корректно работать в 32-битном варианте, но его 64-битный вариант даст сбой.

    При выделении памяти, необходимой для хранения объекта типа MyPointersArray, содержащего 5 указателей, необходимо учесть, что начало массива m_arr будет выровнено по границе 8 байт. Расположение данных в памяти на разных системах (Win32/Win64) показано на рисунке 29.

    Picture 29
    Рисунок 29 — Расположение данных в памяти в 32-битной и 64-битной системе

    Корректный расчет размера должен выглядеть следующим образом:
    struct MyPointersArray {
      DWORD m_n;
      PVOID m_arr[1];
    } object;
    ...
    malloc( FIELD_OFFSET(struct MyPointersArray, m_arr) +
            5 * sizeof(PVOID) );
    ...

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

    Пример 29. Перегруженные функции


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

    Picture 30
    Рисунок 30 — Выбор перегруженной функции в 32-битной и 64-битной системе

    Пример проблемы:

    class MyStack {
    ...
    public:
      void Push(__int32 &);
      void Push(__int64 &);
      void Pop(__int32 &);
      void Pop(__int64 &);
    } stack;
    
    ptrdiff_t value_1;
    stack.Push(value_1);
    ...
    int value_2;
    stack.Pop(value_2);

    Неаккуратный программист помещал и затем выбирал из стека значения различных типов (ptrdiff_t и int). На 32-битной системе их размеры совпадали, все замечательно работало. Когда в 64-битной программе изменился размер типа ptrdiff_t, то в стек стало попадать больше байт, чем затем извлекаться.

    Пример 30. Ошибки в 32-битных модулях, работающих в WoW64


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

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

    lRet = RegOpenKeyEx(HKEY_LOCAL_MACHINE,
      "SOFTWARE\\ODBC\\ODBC.INI\\ODBC Data Sources", 0,
      KEY_QUERY_VALUE,  &hKey);

    Чтобы подружить эту программу с другими 64-битными частями, необходимо вписать ключ KEY_WOW64_64KEY:
    lRet = RegOpenKeyEx(HKEY_LOCAL_MACHINE,
      "SOFTWARE\\ODBC\\ODBC.INI\\ODBC Data Sources", 0,
      KEY_QUERY_VALUE | KEY_WOW64_64KEY,  &hKey);

    Заключение


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

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

    Библиографический список


    1. Андрей Карпов, Евгений Рыжков. Уроки разработки 64-битных приложений на языке Си/Си++. http://www.viva64.com/ru/articles/x64-lessons/
    2. Андрей Карпов. Что такое size_t и ptrdiff_t. http://www.viva64.com/art-1-1-72510946.html
    3. Андрей Карпов, Евгений Рыжков. 20 ловушек переноса Си++ — кода на 64-битную платформу. http://www.viva64.com/art-1-1-1958348565.html
    4. Евгений Рыжков. Учебное пособие по PVS-Studio. http://www.viva64.com/art-4-1-1796251700.html
    5. Андрей Карпов. 64-битный конь, который умеет считать. http://www.viva64.com/art-1-1-1064884779.html
    PVS-Studio
    Static Code Analysis for C, C++, C# and Java

    Comments 19

      +4
      Отличная статья, как и первая. Понравился текст на одной из картинок: «64 bit +good luck» :).
        +3
        Прошу у кого есть, поделиться своими аналогичными примерами.

        И буду рад узнать о замеченных ляпах, опечатках, неточностях и так далее. Но с этим прошу в почту.
          0
          Да как-то я все это уже тут видел, повторю просьбу: мне интересны ошибки при разработке кроссплатформенного софта, то есть такого, который в большем количестве различных ABI работает.
            +1
            Я не знаю си, но статья понравилась. Особенно грузовик с сорванной башней :)
              +1
              Большое и настоящее спасибо.
              Действительно интересные статьи.
                0
                А ваша студия не умеет автоматически исправлять тривиальные ошибки?

                Заменять int на size_t в очевидных случаях достаточно утомительное занятие…
                  +2
                  Не умеет и это сознательное решение.

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

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

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

                  Но конечно было бы прикольно в Visual Studio как в Word мышкой ошибки исправлять подчеркнутые.

                  P.S. Да, и еще язык C++ (бесовский) к подобным тулам (которые на лету что-то правят) очень уж не склонен.
                    +1
                    «Автоматически исправлять» это не значит что прямо вот так делать все за программиста… Это вообще хороший вопрос как такое можно сделать, но, мне кажется, можно придумать систему, которая существенно упростила бы жизнь. Хотя бы какой-то интерактивный помощник, предлагающий варианты исправления для каждого предупреждения.

                    Я осознаю, что в ручную конечно лучше… но «вручную» на проекте более миллиона строк это страшно… учитывая, что size_t нигде нету, так уж повелось…
                      0
                      >> можно придумать систему
                      ни индустрия статических анализаторов в целом, ни мы конкретно не смогли придумать удобного варианта.

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

                      >> «вручную» на проекте более миллиона строк это страшно

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

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

                      Все-таки инструменты для (фактически) рефакторинга C++-кода сейчас намного хуже, чем инструменты для рефакторинга C# (к примеру). А замена типов — это и есть рефакторинг по сути. Помочь мог бы здесь скажем Microsoft с открытием публичного интерфейса в Visual C++ для реализации подобных штук. Они вроде бы даже и заявляют, что хотят со временем открыть это API, но проблема в том, что его сначала нужно сделать :-). Нынешняя реализация Visual C++ (по словам разработчиков) не позволяет легко и просто предоставлять разработчикам сторонних решений подобную функциональность.
                  +2
                  я вот ни разу не c++ программист.

                  но мне кажется большинство указанных тут случаев можно свести к одному правилу:
                  «НЕХУЙ ИСПОЛЬЗОВАТЬ АРИФМЕТИКУ С УКАЗАТЕЛЯМИ!»

                    0
                    А я системный С ++ программист, но с вами абсолютно согласен: если есть возможность не пользоваться указателями — не пользуйтесь ими или сводите операции над указателями к минимуму.

                    Да и большинство ошибок в статье является вариантом адаптированных 32-ух битных.
                      +1
                      Скажу даже более, вообще все примеры можно привести к одной фразе «при использовании арифметики/сдвигов/итд думайте мозгами»

                      Уже не первый десяток лет принята практика «пересборки» старого кода под новые процы и каждый раз одно и тоже. Понатыкают костылей типа #ifdef _WIN32, а результат всё тот-же что 20 лет назад. Cловно учиться на чужих ошибках «не кошерно» надо обязательно на грабли наступить, а может ещё и попрыгать на них…
                        0
                        Я сейчас патчу ядро Linux и не понимаю ваших проблем: что под 32-бита, что под 64-бита! работает!

                        А всего-то сделали что размер long равен размеру void*.
                      0
                      Адресная арифметика активно применялась в те времена, когда не было более безопасных методов, но вот нахуй её сейчас использовать не понимаю. Впрочем те, кому она реально нужна думаю таки знают как её использовать без ошибок
                        0
                        а расскажите о причинах использования арифметики указателей?

                        не таких идиотских, как в примерах, а вообще.

                      –7
                      Слава б-гу не пишу на сях… Одна только мысль, что int остался ограничен 2^31, а вот int* уже 2^63 и из-за этого программа МОЖЕТ работать не корретно, вызывает у меня нервный смех :)
                        +1
                        Оформлено просто замечательно. Спасибо.
                          +1
                          Это мне кажется или до половины проблем связаны с тем, что UINT_PTR и ULONG — разные по размеру типы?
                          В GCC сделано правильнее: long — всегда размером с указатель.
                            0
                            Вторая часть оказалась интереснее, спасибо.
                            Собственно имеется вопрос, зачем вообще все эти, long, long long, long long int, ULONG, UINT_PTR, __int64, size_t, ptrdfiff_t, и прочий геморрой?
                            В SDL хорошо сделано:
                            Uint64, Sint64, Uint32, Uint32 и так далее от 8 бит. IMHO идеальный вариант когда все типы фиксированы.

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