﻿# C\+\+11 и 64\-битные ошибки

64\-битные компьютеры давно и успешно используются\. Большинство приложений стали 64\-битными\.  Это позволяет им использовать больший объем памяти, а также получить прирост производительности за счёт архитектурных возможностей 64\-битных процессоров\. Создание 64\-битных программ на языке Си/Си\+\+ требует от программиста внимательности\. Существует масса причин, из\-за которых код 32\-битной программы отказывается корректно работать после перекомпиляции для 64\-битной системы\. Про это написано много статей\. Но сейчас нам интересен другой вопрос\. Давайте посмотрим, позволяет ли использование новых возможностей, появившихся в C\+\+11, облегчить жизнь программистов, создающих 64\-битные программы\.

Примечание\. Первоначальна статья была опубликована в Software Developer's Journal \(April 25, 2014\)\. Статья публикуется с разрешения редакции\.

![0253_C++11_and_64_bit_issues_ru/image1.png](https://import.viva64.com/docx/blog/0253_C++11_and_64_bit_issues_ru/image1.png)

## Мир 64\-битных ошибок

Существует множество ловушек, в которые может попасть программист, создавая 64\-битные приложения на языке Си/Си\+\+\. Про это написано большое количество статей, поэтому не будем повторяться\. Тем, кто не знаком с нюансами разработки 64\-битных программ или тем, кто хочет освежить свою память, можно порекомендовать следующие ресурсы:

* [Коллекция примеров 64\-битных ошибок в реальных программах](https://pvs-studio.ru/ru/blog/posts/cpp/a0065/);
* [Уроки разработки 64\-битных приложений на языке Си/Си\+\+](https://pvs-studio.ru/ru/blog/lessons/);
* [All about 64\-bit programming in one place](https://software.intel.com/content/www/us/en/develop/blogs/all-about-64-bit-programming-in-one-place.html)\.

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

Статья будет построена следующим образом\. Будет даваться краткое описание типичной 64\-битной ошибки, и предлагаться способы, как её избежать, используя C\+\+11\. Сразу отметим, что далеко не всегда С\+\+11 может хоть чем\-то помочь\. Защитить от ошибок может только аккуратное программирование\. А новый стандарт лишь помогает в этом, но не решить все проблемы за программиста\.

## Магические числа

Речь идёт об использовании таких чисел, как 4, 32, 0x7FFFFFFF, 0xFFFFFFFF \([подробнее](https://pvs-studio.ru/ru/blog/lessons/0009/)\)\. Плохо, если программист предположил, что размер указателя всегда равен 4 байтам и написал вот такой код:

```cpp
int **array = (int **)malloc(n * 4);
```

Здесь стандарт C\+\+11 нам помочь не может\. Магические числа, это зло и единственный способ избежать ошибок \- это стараться их не использовать\.

_**Примечание\.** Да, malloc\(\) это не C\+\+, а старый добрый C\. Намного лучше использовать оператор new или контейнер std::vector\. Но сейчас это к делу не относится\. Разговор про магические числа\._

Впрочем, С\+\+11 иногда помогает сократить число магических чисел\. Некоторые магические числа в программе появляется из\-за боязни \(часто необоснованной\), что компилятор плохо оптимизирует код\. В этом случае, стоит обратить внимание на generalized constant expressions \(сonstexpr\)\.

Механизм **constexpr** гарантирует инициализацию выражений во время компиляции\. При этом, можно объявить функции, которые гарантированно развернутся в константу на этапе компиляции\. Пример:

```cpp
constexpr int Formula(int a) {
  constexpr int tmp = a * 2;
  return tmp + 55;
}
int n = Formula(1);
```

Вызов функции Formula\(1\) превратится в число\. Объяснение конечно слишком краткое\. Подробнее про "constexpr" и другие нововведения можно прочитать, перейдя по ссылкам, приведённым в конце статьи\.

## Функции с переменным количеством аргументов

Речь идёт о неправильном использовании таких функций, как printf, scanf \([подробнее](https://pvs-studio.ru/ru/blog/lessons/0010/)\)\. Пример:

```cpp
size_t value = ....;
printf("%u", value);
```

Этот код корректно работает в 32\-битной программе, но может распечатать некорректные значения, когда программа превратится в 64\-битную\.

Функции с переменным количеством аргументов \- пережиток языка Си\. Их недостаток в отсутствии контроля типов фактических аргументов\. В Си\+\+ уже давно пора отказаться от них\. Есть масса других способов форматирования строк\. Например, можно заменить printf на cout, а sprintf на boost::format или std::stringstream\.

С языком C\+\+11 жизнь стала ещё лучше\. В C\+\+11 появились шаблоны с переменным числом параметров \(Variadic Templates\)\. Это позволяет реализовывать вот такой безопасный вариант функции printf:

```cpp
void printf(const char* s)
{
  while (s && *s) {
    if (*s=='%' && *++s!='%')
      throw runtime_error("invalid format: missing arguments");
    std::cout << *s++;
  }
}
template<typename T, typename... Args>
void printf(const char* s, T value, Args... args)
{
  while (s && *s) {
    if (*s=='%' && *++s!='%') {
      std::cout << value;
      return printf(++s, args...);
    }
    std::cout << *s++;
  }
}
```

Этот код просто "достает" первый аргумент, не являющийся форматной строкой, и затем вызывает себя рекурсивно\. Когда таких аргументов больше не останется, будет вызвана первая \(более простая\) версия метода printf\(\)\.

Тип Args\.\.\. определяет так называемую "группу параметров" \("parameter pack"\)\. По сути, это последовательность пар тип/значение, из которых вы можете "доставать" аргументы, начиная с первого\. При вызове функции printf\(\) с одним аргументом, будет выбран первый метод \(printf\(const char\*\)\)\. При вызове функции printf\(\) с двумя или более аргументами, будет выбран второй метод \(printf\(const char\*, T value, Args\.\.\. args\)\), с первым параметром s, вторым \- value, и оставшиеся параметры \(если они есть\) будут запакованы в группу параметров args, для последующего использования\. При вызове:

```cpp
printf(++s, args...);
```

Группа параметров args сдвигается на один, и следующий параметр может быть обработан в виде value\. И так продолжается до тех пор, пока args не станет пустым \(и будет вызвана первая версия метода printf\(\)\)\.

## Некорректные операции сдвига

Числовой литерал 1 имеет тип int\. Значит, его нельзя сдвигать более чем на 31 разряд \([подробнее](https://pvs-studio.ru/ru/blog/lessons/0011/)\)\. Про это часто забывают, и в программах можно встретить вот такой код:

```cpp
ptrdiff_t mask = 1 << bitNum;
```

Если, значение bitNum будем равно, скажем 40, то результат будет непредсказуем\. Формально, это приведёт к undefined behavior \([подробнее](https://pvs-studio.ru/ru/blog/posts/cpp/0142/)\)\.

Может нам помочь C\+\+11? К сожалению, ничем\.

## Рассинхронизация виртуальных функций

Пусть в базовом классе объявлена виртуальная функция:

```cpp
int A(DWORD_PTR x);
```

А в классе наследнике есть функция:

```cpp
int A(DWORD x);
```

В 32\-битной программе типы DWORD\_PTR и DWORD совпадают\. Однако, в 64\-битной программе, это уже два разных типа \([подробнее](https://pvs-studio.ru/ru/blog/lessons/0012/)\)\. В результате, вызов функции A из базового класса будет приводить к различным результатам в 32\-битной и 64\-битной программе\.

Бороться с подобными ошибками могут помочь новые ключевые слова, появившиеся в C\+\+11\.

Теперь у нас есть ключевое слово **override**, которое позволяет программисту явно выражать свои намерения насчет переопределения функций\. Объявление функции с ключевым словом override является корректным, только если существует функция для переопределения\.

Этот код не скомпилируется в 64\-битном режиме и тем самым ошибка будет обезврежена:

```cpp
struct X
{
  virtual int A(DWORD_PTR) { return 1; }
};
struct Y : public X
{
  int A(DWORD x) override { return 2; }
};
```



## Смешанная арифметика

Это достаточно важная и обширная тема\. Предлагаю познакомиться с соответствующим заделом "64\-битных уроков": [Смешанная арифметика](https://pvs-studio.ru/ru/blog/lessons/0017/)\.

Совсем кратко:

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

### Рассмотрим несколько простых примеров про переполнение

```cpp
char *p = new char[1024*1024*1024*5];
```

Программист пытается выделить массив 5 гигабайт памяти, но выделит гораздо меньше\. Дело в том, что выражение "1024\*1024\*1024\*5" имеет тип int\. В результате произойдёт переполнение, и выражение будет равно 1073741824 \(1 гигабайт\)\. Затем, при передаче в оператор 'new', число 1073741824 будет расширено до типа size\_t, но это не имеет значения \(уже поздно\)\.

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

```cpp
unsigned a = 1024, b = 1024, c = 1024, d = 5;
size_t n = a * b * c * d;
```

Результат выражения помещается в переменную типа 'size\_t'\. Она способна хранить значения больше, чем UINT\_MAX\. Но при перемножении переменных типа 'unsigned' возникает переполнение и результат будет некорректен\.

Почему мы называем всё это 64\-битными ошибками? Дело в том, что в 32\-битной программе невозможно выделить массив размером более 2 гигабайт\. А значит, переполнения просто не возникают\. Проявляют себя такие ошибки только в 64\-битных программах, когда они начинают работать с большими объёмами памяти\.



### Теперь пара примеров про сравнение

```cpp
size_t Count = BigValue;
for (unsigned Index = 0; Index < Count; ++Index)
{ ... }
```

Это пример вечного цикла, если Count \> UINT\_MAX\. Предположим, что на 32\-битных системах этот код выполнял менее повторения, менее чем UINT\_MAX раз\. Но 64\-битный вариант программы может обрабатывать больше данных, и ему может потребоваться большее количество итераций\. Поскольку значения переменной Index лежат в диапазоне \[0\.\.UINT\_MAX\], то условие "Index < Count" всегда выполняется, что и приводит к бесконечному циклу\.

Ещё один пример:

```cpp
string str = .....;
unsigned n = str.find("ABC");
if (n != string::npos)
```

Этот код некорректен\. Функция find\(\) возвращает значение типа string::size\_type\. Всё будет отлично работать в 32\-битной системе\. Но давайте посмотрим, что произойдет в 64\-битной программе\.

В 64\-битной программе string::size\_type и unsigned перестают совпадать\. Если подстрока не находится, функция find\(\) возвращает значение string::npos, которое равно 0xFFFFFFFFFFFFFFFFui64\. Это значение урезается до величины 0xFFFFFFFFu и помещается в 32\-битную переменную\. Вычисляется выражение: 0xFFFFFFFFu \!\= 0xFFFFFFFFFFFFFFFFui64\. Получается, что условие \(n \!\= string::npos\) всегда истинно\!

### Может здесь как\-то помочь C\+\+11?

Ответ \- и да, и нет\.

В некоторых случаях, нам может помочь новое ключевое слово **auto**\. А в некоторых оно может только запутать программиста\. Поэтому, давайте внимательно разберёмся\.

Если объявить "auto a \= \.\.\.\.\.", то её тип будет вычислен автоматически\. Очень важно не запутаться и не написать вот такой **неправильный код**: "auto n \= 1024\*1024\*1024\*5;"\.

Поговорим о ключевом слове **auto**\. Рассмотрим следующий пример:

```cpp
auto x = 7;
```

В данном случае тип переменной 'x' будет 'int', потому что именно такой тип имеет ее инициализатор\. В общем случае мы можем написать:

```cpp
auto x = expression;
```

И тип переменной 'x' будет равен типу значения, полученному в результате вычисления выражения\.

Ключевое слово 'auto' для вывода типа переменной из ее инициализатора, наиболее полезно, когда точный тип выражения не известен, либо сложен в написании\. Рассмотрим пример:

```cpp
template<class T> void printall(const vector<T>& v)
{
  for (auto p = v.begin(); p!=v.end(); ++p)
    cout << *p << "\n";
}
```

В С\+\+98, вам бы пришлось писать гораздо более длинный код:

```cpp
template<class T> void printall(const vector<T>& v)
{
    for (typename vector<T>::const_iterator p = v.begin(); 
         p!=v.end(); ++p)
      cout << *p << "\n";
}
```

Очень полезное нововведение в языке C\+\+11\.

Вернемся к нашей проблеме\. Выражение "1024\*1024\*1024\*5" имеет тип 'int'\. Так что для сейчас нам 'auto' никак не поможет\. 

Не поможет нам 'auto' и в случае цикла:

```cpp
size_t Count = BigValue;
for (auto Index = 0; Index < Count; ++Index)
```

Стало лучше? Нет\. Число 0 имеет тип 'int'\. Значит переменная Index теперь будет иметь тип не 'unsigned', а "int'\. Пожалуй, стало даже хуже\.

**Так есть ли хоть какой\-то нам прок от 'auto'?** Да, есть\. Например, здесь:

```cpp
string str = .....;
auto n = str.find("ABC");
if (n != string::npos)
```

Переменная 'n' будет иметь тип string::size\_type\. Теперь всё хорошо\.

Вот наконец нам и пригодилось новое ключевое слово 'auto'\. Однако, будьте аккуратны\. Нужно понимать, что и зачем вы делаете\. Не надо надеяться побороть все ошибки, связанные со смешанной арифметикой, используя повсеместно 'auto'\. Это всего лишь один из инструментов, а не панацея\.

Кстати, есть ещё один способ защититься от обрезания типа в рассмотренном ранее примере:

```cpp
unsigned n = str.find("ABC");
```

Можно использовать новый формат инициализации переменных, который предотвращает сужение \(narrowing\) типов\. Проблема заключается в том, что языки С и С\+\+ неявно обрезают некоторые типы:

```cpp
int x = 7.3;  // Ой!
void f(int);
f(7.3);  // Ой!
```

Однако списки инициализации С\+\+11 не позволяют сужение \(narrowing\) типов:

```cpp
int x0 {7.3}; //compilation error
int x1 = {7.3}; //compilation error
double d = 7;
int x2{d}; //compilation error
```

Для нас сейчас более интересны вот эти примеры:

```cpp
size_t A = 1;
unsigned X = A;
unsigned Y(A);
unsigned Q = { A }; //compilation error
unsigned W { A }; //compilation error
```



Представим, что код написан так:

```cpp
unsigned n = { str.find("ABC") };
   или так
unsigned n{str.find("ABC")};
```

Этот код будет компилироваться в 32\-битном режиме и перестанет компилироваться в 64\-битном режиме\.

Опять это не панацея от всех ошибок\. Просто ещё один способ писать более надёжные программы\.

## Адресная арифметика

Проблема во многом схожа с тем, что мы рассмотрели в разделе "Смешанная арифметика"\. Отличие лишь в том, что переполнение возникает при работе с указателями \([подробнее](https://pvs-studio.ru/ru/blog/lessons/0013/)\)\.

Рассмотрим пример:

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

Данный код взят из реальной программы математического моделирования, в которой важным ресурсом является объем оперативной памяти\. В программах данного класса для экономии памяти часто используют одномерные массивы, осуществляя работу с ними, как с трехмерными массивами\. Для этого существуют функции, аналогичные GetCell, обеспечивающие доступ к необходимым элементам\. Но приведенный код будет корректно работать только с массивами, содержащими менее INT\_MAX элементов\. Причина \- использование 32\-битных типов int для вычисления индекса элемента\.

Может здесь как\-то помочь C\+\+11? Нет\.

## Изменение типа массива и упаковка указателей

Иногда в программах необходимо \(или просто удобно\) представлять элементы массива в виде элементов другого типа \([подробнее](https://pvs-studio.ru/ru/blog/lessons/0014/)\)\. Ещё бывает удобно хранить указатели в переменных целочисленного типа \([подробнее](https://pvs-studio.ru/ru/blog/lessons/0015/)\)\.

Ошибки возникают из\-за неправильных явных приведений типов\. С новым стандартом С\+\+11 здесь никакой взаимосвязи нет\. Явные приведения типов всегда делались на свой собственный страх и риск\.

Следует ещё упомянуть про работу с данными, находящимися в объединениях \(union\)\. Такая работа с данными является низкоуровневой и также зависит только от умений и знаний программиста \([подробнее](https://pvs-studio.ru/ru/blog/lessons/0016/)\)\.

## Сериализация и обмен данными

В проекте может возникнуть потребность создания совместимого формата данных\. То есть один набор данных должен обрабатываться как 32\-битной, так и 64\-битной версией программы\. Сложность заключается в том, что меняются размеры некоторых типов данных \([подробнее](https://pvs-studio.ru/ru/blog/lessons/0019/)\)\.

Стандарт C\+\+11 немного облегчил жизнь, введя типы фиксированного размера\. Раньше программисты объявляли такие типы самостоятельно или использовали типы, объявленные в одной из системной библиотек\.

Теперь есть следующие типы фиксированного размера:

* int8\_t 
* int16\_t
* int32\_t
* int64\_t
* uint8\_t 
* uint16\_t
* uint32\_t
* uint64\_t



Кроме размера изменяются выравнивание данных в памяти \(data alignment\)\. Это тоже может предоставить определённые сложности \([подробнее](https://pvs-studio.ru/ru/blog/lessons/0021/)\)\. 

Касательно этой темы, стоит упомянуть появление в С\+\+11 нового ключевого слова 'alignment'\. Теперь можно написать вот такой код:

```cpp
// массив символов, выровнен для хранения типов double
alignas(double) unsigned char c[1024]; 
// выравнивание по 16 байтной границе
alignas(16) char[100];
```

Существует также оператор 'alignof', который возвращает выравнивание для указанного аргумента \(аргумент должен быть типом\)\. Пример:

```cpp
constexpr int n = alignof(int);
```

## Перегруженные функции

При переносе 32\-битных программ на 64\-битную платформу может наблюдаться изменение логики ее работы, связанное с использованием перегруженных функций\. Если функция перекрыта для 32\-битных и 64\-битных значений, то обращение к ней с аргументом, например, типа size\_t будет транслироваться в различные вызовы на различных системах \([подробнее](https://pvs-studio.ru/ru/blog/lessons/0022/)\)\.

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

## Проверки размеров типа

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

Часто это делают неправильным способом\. Например, так:

```cpp
assert(sizeof(unsigned) < sizeof(size_t));
assert(sizeof(short) == 2);
```

Плохой способ\. Во\-первых, программа всё равно компилируется\. Во\-вторых, эти проверки проявят себя только в отладочной версии\.

Гораздо лучше останавливать компиляцию, если необходимые условия не выполняются\. Для этого существует множество решений\. Например, можно использовать макрос \_STATIC\_ASSERT, который доступен разработчикам, использующим Visual Studio\. Пример использования:

```cpp
_STATIC_ASSERT(sizeof(int) == sizeof(long));
```

C\+\+11 стандартизировал способ, как остановить компиляцию, если что\-то пошло не так\. В язык введены утверждения времени компиляции \(static assertions\)\.

Статические утверждения \(утверждения времени компиляции\) содержат константное выражение и строковый литерал:

```cpp
static_assert(expression, string);
```

Компилятор вычисляет выражение, и если результат вычисления равен false \(т\.е\. утверждение нарушено\), выводит строку в качестве сообщения об ошибке\. Примеры:

```cpp
static_assert(sizeof(size_t)>=8, 
  "64-bit code generation required for this library.");

struct S { X m1; Y m2; };
static_assert(sizeof(S)==sizeof(X)+sizeof(Y),
  "unexpected padding in S");
```



## Заключение

Написание кода с максимальным использованием новых конструкций языка C\+\+11 вовсе не гарантирует отсутствия 64\-битных ошибок\. Однако, язык представляет несколько новых возможностей, которые позволят сделать код более коротким и надёжным\.

## Дополнительные ресурсы

В статье не делалась попытка ознакомить читателя как с можно большим количеством нововведений в языке C\+\+11\. Для первого знакомства с новым стандартам можно порекомендовать следующие ресурсы:

1. Wikipedia\. [C\+\+11](https://en.wikipedia.org/wiki/C%2B%2B11)\.
1. Scott Meyers\. [An Effective C\+\+11/14 Sampler](https://channel9.msdn.com/Events/GoingNative/2013/An-Effective-Cpp11-14-Sampler)\.