Преобразования типов и безопасность типов

Этот документ определяет распространенные проблемы преобразования типов и описывает, как их можно избежать в коде 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

Подписанные — незаписанные преобразования

Подписанный целочисленный тип и его неподписанный аналог всегда одинаковый, но они отличаются в том, как битовый шаблон интерпретируется для преобразования значений. В следующем примере кода показано, что происходит, когда тот же битовый шаблон интерпретируется как подписанное значение и как неподписаемое значение. Битовый шаблон, хранящийся в обоих num и num2 никогда не изменяется от того, что показано на предыдущем рисунке.

using namespace std;
unsigned short num = numeric_limits<unsigned short>::max(); // #include <limits>
short num2 = num;
cout << "unsigned val = " << num << " signed val = " << num2 << endl;
// Prints: "unsigned val = 65535 signed val = -1"

// Go the other way.
num2 = -1;
num = num2;
cout << "unsigned val = " << num << " signed val = " << num2 << endl;
// Prints: "unsigned val = 65535 signed val = -1"

Обратите внимание, что значения переосмыслены в обоих направлениях. Если программа создает нечетные результаты, в которых знак значения по-видимому, повернут из ожидаемого значения, найдите неявные преобразования между подписанными и неподписанными целочисленными типами. В следующем примере результат выражения (0 – 1) неявно преобразуется из intunsigned int того, когда он хранится.num Это приводит к повторному интерпретации битового шаблона.

unsigned int u3 = 0 - 1;
cout << u3 << endl; // prints 4294967295

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

Преобразования указателей

Во многих выражениях массив стилей C неявно преобразуется в указатель на первый элемент в массиве, а преобразования констант могут выполняться автоматически. Хотя это удобно, это также потенциально подвержено ошибкам. Например, следующий плохо разработанный пример кода кажется нечувствичным, но он компилирует и создает результат p. Во-первых, константный литерал строки help преобразуется char* в тот, который указывает на первый элемент массива. Затем указатель увеличивается тремя элементами, чтобы он указывал на последний элемент p.

char* s = "Help" + 3;

Явные преобразования (приведение)

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

В программировании в стиле 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<int>(d);       // No warning.
    string s = static_cast<string>(d); // Error C2440:cannot convert from
                                       // double to std:string
    
    // No error but not necessarily safe.
    Base* b = new Base();
    Derived* d2 = static_cast<Derived*>(b);
    

    Дополнительные сведения см. в разделе static_cast.

  • dynamic_cast, для безопасных, проверка проверка приведения указателя на базу к указателю на производный. Это dynamic_cast безопаснее, чем static_cast для даункастов, но среда выполнения проверка несет некоторые издержки.

    Base* b = new Base();
    
    // Run-time check to determine whether b is actually a Derived*
    Derived* d3 = dynamic_cast<Derived*>(b);
    
    // If b was originally a Derived*, then d3 is a valid pointer.
    if(d3)
    {
       // Safe to call Derived method.
       cout << d3->DoSomethingMore() << endl;
    }
    else
    {
       // Run-time check failed.
       cout << "d3 is null" << endl;
    }
    
    //Output: d3 is null;
    

    Дополнительные сведения см. в разделе dynamic_cast.

  • const_cast, для отведения constот -ness переменной или преобразованияconst не-переменной в значение const. Отбрасывание const-ness с помощью этого оператора так же подвержено ошибкам, как и использование приведения в стиле C, за исключением того, что с const_cast вами меньше шансов случайно выполнить приведение. Иногда необходимо отбросить const-ness переменной, например, чтобы передать const переменную в функцию, которая принимает параметр, отличныйconst от параметра. В приведенном ниже примере показано, как это сделать.

    void Func(double& d) { ... }
    void ConstCast()
    {
       const double pi = 3.14;
       Func(const_cast<double&>(pi)); //No error.
    }
    

    Дополнительные сведения см. в разделе const_cast.

  • reinterpret_castдля приведения между несвязанными типами, такими как тип указателя и объект int.

    Примечание.

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

    В следующем примере показано, как reinterpret_cast отличается от static_cast.

    const char* str = "hello";
    int i = static_cast<int>(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<int>(str);// Programming intent is clear.
                                       // However, it is not 64-bit safe.
    

    Дополнительные сведения см. в разделе reinterpret_cast "Оператор".

См. также

Система типов C++
Возвращение к C++
Справочник по языку C++
Стандартная библиотека C++