Этот раздел является переводом туториала 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 (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.