PVOID - Преобразование типов

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

Этот раздел является переводом туториала C++ Language


 

Неявные преобразования


Неявные преобразования выполняются автоматически, когда значение копируется в совместимый тип. Например:

short a = 2000;
int b;
b = a;

 Здесь значение a преобразуется из short в int без необходимости явного применения какого-либо оператора. Это известно как стандартное преобразование. Стандартное преобразование применимо к фундаментальным типам данных и позволяет совершать преобразования между числовыми типами (short в int, int в float, double в int...), в или из bool, и некоторые преобразования указателей.

Преобразование в int из некоторых меньших целочисленных типов или в double из float гарантированно произведет точно такое же значение целевого типа. Другие преобразования между арифметическими типами не всегда приводят к точно такому же результирующему значению:

  • Если отрицательное целочисленное значение преобразуется в беззнаковый тип, результирующее значение соответствует его побитовому представлению в дополнительном коде (т.е. -1 становится наибольшим значением, представляемым типом, -2 вторым наибольшим, …).
  • Преобразование в/из bool рассматривает false эквивалентом нуля (для числовых типов) и пустым указателем (null pointer) для типов указателей; true эквивалентно всем другим значениям и рассматривается эквивалентом 1.
  • Если преобразование производится из типа с плавающей точкой в целочисленный тип, значение обрезается (дробная часть отбрасывается). Если результат лежит вне диапазона значений типа, преобразование приводит к неопределенному поведению.
  • Тем не менее, если производится преобразование между числовыми типами одного рода (целое в целое или значение с плавающей точкой в значение с плавающей точкой), преобразование является допустимым, но значение зависит от реализации (и может не быть переносимым).

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

Для нефундаментальных типов, массивы и функции неявно преобразуются в указатели, а указатели в общем случае позволяют следующие преобразования:

  • Пустые указатели могут преобразованы в указатели любого типа;
  • Указатели любого типа могут быть преобразованы в указатели на void;
  • Приведение указателей вверх: указатели на производные классы могут быть преобразованы в указатели на доступный и недвусмысленный базовый класс, без изменения квалификаторов const или volatile.

 

Неявное приведение типов


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

  • конструктор с одним аргументом: позволяет неявные преобразования из конкретного типа при инициализации объекта;
  • оператор присваивания: позволяет неявные преобразования из конкретного типа при присваивании;
  • оператор приведения типа: позволяет неявные преобразования из конкретного типа.

Например:

// неявное преобразование классов:
#include <iostream>
using namespace std;

class A {};

class B {
public:
  // преобразование из A (конструктор):
  B (const A& x) {}
  // преобразование из A (присваивание):
  B& operator= (const A& x) {return *this;}
  // преобразование из A (оператор приведения типа)
  operator A() {return A();}
};

int main ()
{
  A foo;
  B bar = foo;    // конструктор
  bar = foo;      // присваивание 
  foo = bar;      // приведение типов
  return 0;
}

Оператор приведения типа использует особый синтаксис: он использует ключевое слово operator, за которым следует целевой тип и пустая пара круглых скобок. Обратите внимание, что возвращаемым типом является целевой тип, поэтому он не указывается перед ключевым словом operator.

 

Ключевое слово explicit


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

void fn (B arg) {}

Эта функция принимает аргумент типа B, но она также может быть вызвана с объектом типа A в качестве аргумента:

fn (foo);

 Это может быть, но может и не быть тем, что имелось в виду. Но, в любом случае, это может быть предотвращено путем объявления упомянутого конструктора с ключевым словом explicit:

// explicit:
#include <iostream>
using namespace std;

class A {};

class B {
public:
  explicit B (const A& x) {}
  B& operator= (const A& x) {return *this;}
  operator A() {return A();}
};

void fn (B x) {}

int main ()
{
  A foo;
  B bar (foo);
  bar = foo;
  foo = bar;
  
//  fn (foo);  // не допускается explicit конструктором.
  fn (bar);  

  return 0;
}

 В дополнение, конструкторы, отмеченные как explicit, не могут быть вызваны через присваивание; в примере выше, bar не может быть инициализирован как:

B bar = foo;

 Методы приведения типов (которые описаны в предыдущей секции) также могут быть определены как explicit. Это предотвращает неявные преобразования таким же образом, что и explicit-конструкторы делают для целевого типа.

 

Приведение типов


C++ является строго типизированным языком. Многие преобразования, особенно те, что подразумевают различные интерпретации значения, требуют явного преобразования, известного в C++ как приведение типов. Существует два основных синтаксиса для приведения типов: си-подобный и функциональный:

double x = 10.3;
int y;
y = int (x);    // преобразование в функциональном стиле
y = (int) x;    // преобразование в стиле Си

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

// приведение типов в классах
#include <iostream>
using namespace std;

class Dummy {
    double i,j;
};

class Addition {
    int x,y;
  public:
    Addition (int a, int b) { x=a; y=b; }
    int result() { return x+y;}
};

int main () {
  Dummy d;
  Addition * padd;
  padd = (Addition*) &d;
  cout << padd->result();
  return 0;
}

Здесь объявляется указатель на Addition, а затем ему присваивается ссылка на объект другого несвязанного типа с использованием явного приведения типа:

padd = (Addition*) &d;

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

Для управления такими типами преобразований между классами, существует четыре оператора: dynamic_cast, reinterpret_cast, static_cast и const_cast. За каждым из операторов следует новый тип, заключенный в угловые скобки (<>), а затем преобразуемое выражение в круглых скобках:

 dynamic_cast <new_type> (expression)
 reinterpret_cast <new_type> (expression)
 static_cast <new_type> (expression)
 const_cast <new_type> (expression)

 Традиционное приведение типов, эквивалентное этим выражениям, выглядит так:

 (new_type) expression
 new_type (expression)

 но каждый из этих операторов имеет свои особенности:

 

dynamic_cast


 dynamic_cast можно использовать только с указателями и ссылками на классы (или с void*). Его цель — убедиться, что результат преобразования типа указывает на корректный полный объект целевого типа.
 Это, естественно, включает восходящее преобразование указателей — upcast (преобразование из указателя на производный класс к указателю на базовый класс) таким же образом, как это разрешено для неявного преобразования.
 Но dynamic_cast позволяет также производить нисходящее преобразование — downcast (из указателя на базовый класс к указателю на производный класс) полиморфных классов (имеющих виртуальные члены) только тогда, когда объект является корректным полным объектом целевого типа. Например:

// dynamic_cast
#include <iostream>
#include <exception>
using namespace std;

class Base { virtual void dummy() {} };
class Derived: public Base { int a; };

int main () {
  try {
    Base * pba = new Derived;
    Base * pbb = new Base;
    Derived * pd;

    pd = dynamic_cast<Derived*>(pba);
    if (pd==0) cout << "Null pointer on first type-cast.\n";

    pd = dynamic_cast<Derived*>(pbb);
    if (pd==0) cout << "Null pointer on second type-cast.\n";

  } catch (exception& e) {cout << "Exception: " << e.what();}
  return 0;
}
Null pointer on second type-cast.

 Замечание о совместимости: этот тип dynamic_cast требует Run-Time Type Information (RTTI) для отслеживания динамических типов. Некоторые компиляторы поддерживают эту возможность как опцию, выключенную по умолчанию. Необходимо, чтобы эта опция была включена для проверки типов времени исполнения с использованием dynamic_cast для правильной работы с этими типами.


 Приведенный выше код пытается выполнить два приведения типов из указателей типа Base* (pba и pbb) к указателю типа Derived*, но только первое из них успешно. Обратите внимание на их соответствующие инициализации:

Base * pba = new Derived;
Base * pbb = new Base;

 Хотя оба указателя типа Base*, pba в действительности указывает на объект типа Derived, в то время как pbb указывает на объект типа Base. Поэтому, когда соответствующие приведения типов выполняются с использованием dynamic_cast, pba указывает на полный объект класса Derived, в то время как pbb указывает на объект класса Base, который является неполным объектом класса Derived.

 Когда dynamic_cast не может привести указатель, поскольку он не является полным объектом требуемого класса — как во втором преобразовании в предыдущем примере — он возвращает пустой указатель, указывающий на ошибку. Если dynamic_cast используется для преобразования в ссылочный тип и преобразование невозможно, вместо этого выбрасывается исключение типа bad_cast.

 dynamic_cast также может выполнять другие неявные приведения, разрешенные для указателей: приведение нулевых указателей между различными типами указателей (даже между несвязанными классами) и приведение любого указателя любого типа к указателю void*.

 

static_cast


 static_cast может выполнять приведение между указателями на связанные классы, не только приведение вверх (upcast) (от указателя на производный класс к указателю на базовый класс), но также и приведение вниз (downcast), (от указателя на базовый класс к указателю на производный класс). Во время выполнения не выполняется никаких проверок, чтобы гарантировать, что преобразуемый объект фактически является полным объектом целевого типа. Поэтому программист должен убедиться, что преобразование безопасно. С другой стороны, это не приводит к накладным расходам на проверку безопасности типов как при использовании dynamic_cast.

class Base {};
class Derived: public Base {};
Base * a = new Base;
Derived * b = static_cast<Derived*>(a);

  Это будет корректный код, хотя b будет указывать на неполный объект класса и может привести к ошибкам времени исполнения при разыменовании.

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

 static_cast также способен производить все преобразования, разрешенные неявно (не только с указателями на классы), но также способен произвоить преобразования, обратные им. Он может:

  • преобразовать из void* к любому типу указателя. В этом случае, о гарантирует, что если значение void* было получено путем преобразования из указателя того же типа, значение результирующего указателя будет таким же;
  • преобразовывать целочисленные значения, значения с плавающей точкой и значения типа перечислений (enum) к типам перечислений (enum).

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

  • производить явный вызов конструктора с одним аргументом или оператора приведения типа;
  • выполнять приведение к rvalue ссылкам;
  • выполнять приведение значений enum class к целочисленным или вещественным значениям;
  • выполнять приведение любого типа к void, оценивая и отбрасывая значение.

 

reinterpret_cast


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

 Так же он может приводить значения указателей к целочисленным значениям и наоборот. Формат, в котором это целочисленное значение представляет указатель, является платформозависимым. Гарантируется только то, что указатель, приведенный к целочисленному значению, тип которого достаточно большой, чтобы полностью вместить указатель (такой как intptr_t), гарантированно может быть приведен обратно к действительному указателю.

 Преобразования, которые могут быть совершены reinterpret_cast, но не static_cast, — это низкоуровневые операции, основанные на изменении интерпретации двоичного представления типов, которые в большинстве случаев приводят к созданию непереносимого платформозависимого кода. Например:

class A { /* ... */ };
class B { /* ... */ };
A * a = new A;
B * b = reinterpret_cast<B*>(a);

 Этот код компилируется, однако, не имеет особого смысла, так как теперь b указывает на объект совершенно несвязанного и, вероятно, несовместимого класса. Разыменование b является небезопасным.

 

const_cast


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

// const_cast
#include <iostream>
using namespace std;

void print (char * str)
{
  cout << str << '\n';
}

int main () {
  const char * c = "sample text";
  print ( const_cast<char *> (c) );
  return 0;
}
sample text

 Пример выше гарантированно сработает, потому что функция print ничего не записывает в объект, на который указывает str. Обратите внимание, что удаление константности адресуемого объекта для действительной записи приведет к неопределенному поведению.

 

typeid


 typeid позволяет проверить тип выражения:

 typeid (expression)

 Этот оператор возвращает ссылку на константный объект типа type_info, который определен в стандартном заголовочном файле <typeinfo>. Значение, возвращаемое typeid, может быть сравнено с другим значением, возвращенным typeid, с использованием операторов == и !=, или может служить для получения последовательности символов с завершающим нулем, представляющей тип данных или имя класса, путем использования его метода name().

// typeid
#include <iostream>
#include <typeinfo>
using namespace std;

int main () {
  int * a,b;
  a=0; b=0;
  if (typeid(a) != typeid(b))
  {
    cout << "a and b are of different types:\n";
    cout << "a is: " << typeid(a).name() << '\n';
    cout << "b is: " << typeid(b).name() << '\n';
  }
  return 0;
}
a and b are of different types:
a is: int *
b is: int

 Когда typeid применяется к классам, typeid использует RTTI для отслеживания типа динамических объектов. Когда typeid применяется к выражению, тип которого является полиморфным классом, результатом является тип самого нижнего в иерархии классов полного объекта:

// typeid, polymorphic class
#include <iostream>
#include <typeinfo>
#include <exception>
using namespace std;

class Base { virtual void f(){} };
class Derived : public Base {};

int main () {
  try {
    Base* a = new Base;
    Base* b = new Derived;
    cout << "a is: " << typeid(a).name() << '\n';
    cout << "b is: " << typeid(b).name() << '\n';
    cout << "*a is: " << typeid(*a).name() << '\n';
    cout << "*b is: " << typeid(*b).name() << '\n';
  } 
  catch (exception& e) 
  { cout << "Exception: " << e.what() << '\n'; }
  return 0;
a is: class Base *
b is: class Base *
*a is: class Base
*b is: class Derived

 Замечание: строка, возвращаемая методом name класса type_info, зависит от конкретной реализации компилятора и библиотек. Это не обязательно простая строка с обычным именем типа, как в компиляторе, используемом для создания этого вывода.

 Обратите внимание, что тип, который typeid рассматривает для указателей, является типом самих указателей (как a, так и b имеют тип class Base *). Тем не менее, когда typeid применяется к объектам (таким, как *a и *b), typeid возвращает их динамический тип ( то есть тип самого нижнего в иерархии классов полного объекта).

 Если тип, который оценивает typeid является указателем, которому предшествует оператор разыменования (*), и этот указатель имеет нулевое значение, typeid генерирует исключение bad_typeid.