Арифметические преобразования типов

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

Целочисленное расширение подразумевает преобразование bool, char, unsigned char, short, unsigned short к int (или unsigned int, если int недостаточно для хранения конкретных чисел). Большие значения могут преобразовываться к long и unsigned long.

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

double d = 1.0;
int x = 1.0 / 10// truncation of constant value
int y = d / 10;   // possible loss of data due to type conversion

Выражение для инициализации переменных x и y содержит вещественное число 1.0, поэтому другие операнды (в данном случае, константы 10) преобразуются в double, и результат деления также будет типа double. Однако тип переменных — int, и потому происходит неявное преобразование к нему.

Вычисление 1.0 / 10 компилятор выполняет во время компиляции и потому получает константу типа double (0.1). Конечно, на практике маловероятен такой код, чтобы инициализирующая константа превышала размер приемной переменной. Поэтому предупреждение компилятора "усечение константы" ("truncation of constant value") можно считать экзотическим. Просто оно демонстрирует проблему в наиболее упрощенном виде.

Однако в результате вычислений на основе переменных аналогичная потеря данных также может произойти. Второе предупреждение компилятора, которое мы здесь видим ("возможна потеря точности из-за конвертации типов" — "possible loss of data due to type conversion"), возникает гораздо чаще. Причем потеря возможна не только при приведении из вещественного типа в целый, но и обратно.

double f = LONG_MAX// truncation of constant value
long m1 = 1000000000;
f = m1 * m1;         // possible loss of data due to type conversion

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

Еще одно предупреждение, с которым мы можем столкнуться из-за несоответствия типов: "переполнение целой константы" ("integral constant overflow").

long m1 = 1000000000;
long m2 = m1 * m1;                 // ok: m2 = 1000000000000000000
long m3 = 1000000000 * 1000000000// integral constant overflow
                                   // m3 = -1486618624

Целочисленные константы имеют в MQL5 тип int, поэтому умножение миллиона на миллион выполняется с учетом диапазона этого типа, а он равен INT_MAX (2147483647). Значение 1000000000000000000 вызывает переполнение, и в m3 попадает остаток от деления этого значения на диапазон (подробнее об этом — во врезке ниже).

То, что приемная переменная m3 имеет тип long — не означает, что значения в выражении должны заранее приводиться к нему. Это происходит только в момент присваивания. Для того чтобы умножение выполнялось по правилам long, требуется каким-либо образом указать тип long непосредственно в самом выражении. Это можно сделать с помощью явного приведения или путем использования переменных. В частности, получение того же произведения с помощью переменной m1 типа long (как m1 * m1) приводит к правильному результату в m2.

Знаковые и беззнаковые целые
 
Программы не всегда пишутся идеально, с защитой от всех возможных сбоев. Поэтому иногда бывает, что получившееся в ходе вычислений целое число не умещается в переменную выбранного целого типа. Тогда в неё попадает остаток от деления этого значения на максимальную величину (M), которую можно записать в соответствующее количество байтов (размер типа), плюс 1. Так для целых типов размером от 1 до 4 байтов M+1 составляет, соответственно, 256, 65536, 4294967296 и 18446744073709551616.
 
Но для знаковых типов есть нюанс. Как мы знаем, у знаковых чисел общий диапазон значений поделен примерно поровну между положительной и отрицательной областями. Поэтому новое "остаточное" значение может в 50% случаев превысить положительный или отрицательный лимит. В таком случае число превращается в "противоположное": у него меняется знак и оно оказывается на удалении M от исходного.
 
Важно понимать, что это превращение происходит только из-за разной интерпретации состояния битов во внутреннем представлении, а само состояние одно и то же для знаковых и беззнаковых чисел.
 
Поясним на примере для самых маленьких целых типов: char и uchar.
 
Так как unsigned char способен хранить значения от 0 до 255, то 256 отображается в 0, -1 — в 255, 300 — в 44, и так далее. При попытке записать 300 в обычный знаковый char мы также получим 44, потому что 44 лежит в диапазоне от 0 до 127 (положительная область значений char). Однако если присвоить переменным char и uchar значение 3000, картина получится разная. Остаток от деления 3000 на 256 равен 184. Он попадает в uchar без изменений. Однако для char точно такое же сочетание битов дает -72. Нетрудно проверить, что 184 и -72 отличаются на 256.

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

char c = 3000;      // truncation of constant value
Print(c);           // -72
uchar uc = 3000;    // truncation of constant value
Print(uc);          // 184

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

char c55 = 55;
char sm = c55 * c55;  // ok! 
Print(sm);            // 3025 -> -47
uchar um = c55 * c55// ok!
Print(um);            // 3025 -> 209

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

uint u = 11;
int i = -49;
Print(i + i); // -98
Print(u + i); // 4294967258 = 4294967296 - 38

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

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

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