Преобразования типов и безопасность типов
В этом документе описываются распространенные проблемы с преобразованием типов и описывается, как избежать их в коде C++.
При написании программы C++ важно убедиться, что она является типобезопасной. Это означает, что каждая переменная, аргумент функции и возвращаемое значение функции хранит допустимый тип данных, и что операции со значениями разных типов «имеют смысл» и не приводят к потере данных, неправильной интерпретации битовых шаблонов или повреждению памяти. Программа, которая никогда явно или неявно не преобразует значения из одного типа в другой, является типобезопасной по определению. Однако иногда требуются преобразования типов, даже небезопасные. Например, может потребоваться сохранить результат операции с плавающей запятой в переменной типа int или передать значение в unsigned int функции, принимающей signed int . Оба примера иллюстрируют небезопасные преобразования, так как они могут привести к потере данных или повторной интерпретации значения.
Когда компилятор обнаруживает небезопасное преобразование, он выдает ошибку или предупреждение. Ошибка останавливает компиляцию; предупреждение позволяет продолжить компиляцию, но указывает на возможную ошибку в коде. Однако даже если программа компилируется без предупреждений, она по-прежнему может содержать код, который приводит к неявным преобразованиям типов, которые дают неверные результаты. Ошибки типов также могут быть вызваны явными преобразованиями или приведениями в коде.
Неявные преобразования типов
Если выражение содержит операнды различных встроенных типов и явные приведения отсутствуют, компилятор использует встроенные стандартные преобразования для преобразования одного из операндов, чтобы типы соответствовали. Компилятор пытается выполнить преобразования в четко определенной последовательности, пока не будет выполнено успешное преобразование. Если выбранное преобразование является повышением, компилятор не выдает предупреждение. Если преобразование является сужающим, компилятор выдает предупреждение о возможной потере данных. Фактическая потеря данных зависит от фактических значений, но мы рекомендуем рассматривать это предупреждение как ошибку. Если используется определяемый пользователем тип, компилятор пытается использовать преобразования, указанные в определении класса. Если не удается найти допустимое преобразование, компилятор выдает ошибку и не компилирует программу. Дополнительные сведения о правилах, регулирующих стандартные преобразования, см. в разделе Стандартные преобразования. Дополнительные сведения о пользовательских преобразованиях см. в разделе Определяемые пользователем преобразования (C++/CLI).
Расширение конверсий (повышение)
При расширении преобразования значение в меньшей переменной присваивается более крупной переменной без потери данных. Так как расширяющие преобразования всегда безопасны, компилятор выполняет их автоматически и не выдает предупреждений. Следующие преобразования представляют собой расширяющие преобразования.
Исходный тип | Кому |
---|---|
Любой signed тип или unsigned целочисленный, кроме long long или __int64 | double |
bool или char | Любой другой встроенный тип |
short или wchar_t | int , long , long long |
int , long | long long |
float | double |
Сужающие преобразования (приведение)
Компилятор выполняет сужающие преобразования неявно, но предупреждает о возможной потере данных. Относимся к этим предупреждениям очень серьезно. Если вы уверены, что потеря данных не произойдет, так как значения в переменной большего размера всегда помещаются в меньшую переменную, добавьте явное приведение, чтобы компилятор больше не выводил предупреждение. Если вы не уверены, что преобразование является безопасным, добавьте в код какой-либо тип среды выполнения проверка для обработки возможной потери данных, чтобы не привести к неправильным результатам программы.
Любое преобразование типа с плавающей запятой в целочисленный тип является сужающим преобразованием, так как дробная часть значения с плавающей запятой отбрасывается и теряется.
В следующем примере кода показаны некоторые неявные сужающие преобразования и предупреждения, которые компилятор выдает для них.
int i = INT_MAX + 1; //warning C4307:'+':integral constant overflow wchar_t wch = 'A'; //OK char c = wch; // warning C4244:'initializing':conversion from 'wchar_t' // to 'char', possible loss of data unsigned char c2 = 0xfffe; //warning C4305:'initializing':truncation from // 'int' to 'unsigned char' int j = 1.9f; // warning C4244:'initializing':conversion from 'float' to // 'int', possible loss of data int k = 7.7; // warning C4244:'initializing':conversion from 'double' to // 'int', possible loss of data
Signed — неподписанные преобразования
Целочисленный тип со знаком и его неподписанный аналог всегда имеют одинаковый размер, но они различаются тем, как битовый шаблон интерпретируется для преобразования значений. В следующем примере кода показано, что происходит, когда один и тот же битовый шаблон интерпретируется как значение со знаком и как неподписаное значение. Битовый шаблон, хранящийся в обоих num и num2 никогда не изменяется от того, что показано на предыдущем рисунке.
using namespace std; unsigned short num = numeric_limits::max(); // #include short num2 = num; cout
Обратите внимание, что значения повторно интерпретируются в обоих направлениях. Если программа выдает нечетные результаты, в которых знак значения кажется инвертирован от ожидаемого, найдите неявные преобразования между целочисленными типами со знаком и без знака. В следующем примере результат выражения ( от 0 до 1) неявно преобразуется из int в unsigned int , если он хранится в num . Это приводит к повторному толкованию битового шаблона.
unsigned int u3 = 0 - 1; cout
Компилятор не предупреждает о неявных преобразованиях между целочисленными типами со знаком и без знака. Поэтому рекомендуется полностью избегать преобразования со знаком в беззнаковые. Если их избежать не удается, добавьте проверка среды выполнения, чтобы определить, больше или равно ли преобразуемое значение нулю и меньше или равно максимальному значению типа со знаком. Значения в этом диапазоне будут передаваться из подписанного в неподписанный или из неподписанных в подписанный без повторного интерпретации.
Преобразования указателей
Во многих выражениях массив в стиле C неявно преобразуется в указатель на первый элемент массива, и преобразования констант могут выполняться автоматически. Хотя это удобно, это также потенциально подвержено ошибкам. Например, следующий плохо спроектированный пример кода кажется бессмысленным, но он будет компилироваться и выдавать результат "p". Сначала строковый литерал "Help" преобразуется в , указывающий char* на первый элемент массива; затем указатель увеличивается на три элемента, чтобы он указывал на последний элемент "p".
Явные преобразования (приведения)
С помощью операции приведения можно указать компилятору преобразовать значение одного типа в другой тип. Компилятор в некоторых случаях вызывает ошибку, если два типа полностью не связаны между собой, но в других случаях он не вызовет ошибку, даже если операция не является типобезопасной. Приведения следует использовать редко, так как любое преобразование из одного типа в другой является потенциальным источником ошибки программы. Однако иногда требуются приведения, и не все приведения одинаково опасны. Одно из эффективных применений приведения — это когда код выполняет сужающее преобразование и вы знаете, что преобразование не приводит к тому, что программа выдает неверные результаты. Фактически это сообщает компилятору, что вы знаете, что делаете, и перестаете беспокоить вас с предупреждениями об этом. Другой способ использования — приведение из указателя на производный класс к классу указателя на базовый. Другой способ использования — отбрасывает константность переменной, чтобы передать ее в функцию, требующую аргумента, не являющегося константным. Большинство из этих операций приведения сопряжены с некоторым риском.
В программировании в стиле C для всех типов приведения используется один и тот же оператор приведения в стиле C.
(int) x; // old-style cast, old-style syntax int(x); // old-style cast, functional syntax
Оператор приведения в стиле C идентичен оператору вызова () и поэтому является незаметным в коде и легко упускается из виду. Оба они плохо, потому что их трудно распознать с первого взгляда или найти, и они достаточно разрозненные, чтобы вызвать любое сочетание static , const и reinterpret_cast . Выяснить, что на самом деле делает старый стиль приведения может быть трудным и подверженным ошибкам. По всем этим причинам, если требуется приведение, рекомендуется использовать один из следующих операторов приведения C++, которые в некоторых случаях являются значительно более типобезопасными и которые гораздо более явно выражают цель программирования:
- static_cast — для приведения, которые проверяются только во время компиляции. static_cast возвращает ошибку, если компилятор обнаруживает, что вы пытаетесь выполнить приведение между полностью несовместимыми типами. Его также можно использовать для приведения между указателем на базу и указателем на производный, но компилятор не всегда может определить, будут ли такие преобразования безопасными во время выполнения.
double d = 1.58947; int i = d; // warning C4244 possible loss of data int j = static_cast(d); // No warning. string s = static_cast(d); // Error C2440:cannot convert from // double to std:string // No error but not necessarily safe. Base* b = new Base(); Derived* d2 = static_cast(b);
Base* b = new Base(); // Run-time check to determine whether b is actually a Derived* Derived* d3 = dynamic_cast(b); // If b was originally a Derived*, then d3 is a valid pointer. if(d3) < // Safe to call Derived method. cout DoSomethingMore() else < // Run-time check failed. cout << "d3 is null" //Output: d3 is null;
Примечание Этот оператор приведения используется не так часто, как остальные, и не гарантирует его переносимость в другие компиляторы.
В следующем примере показано, чем reinterpret_cast отличается от static_cast .
const char* str = "hello"; int i = static_cast(str);//error C2440: 'static_cast' : cannot // convert from 'const char *' to 'int' int j = (int)str; // C-style cast. Did the programmer really intend // to do this? int k = reinterpret_cast(str);// Programming intent is clear. // However, it is not 64-bit safe.