Pull to refresh

Еще раз про приведение типов в языке С++ или расстановка всех точек над cast

Reading time8 min
Views116K

Этот пост попытка кратко оформить все, что я читал или слышал из разных источников про операторы приведения типов в языке C++. Информация ориентирована в основном на тех, кто изучает C++ относительно недолго и, как мне кажется, должна помочь понять cпецифику применения данных операторов. Старожилы и гуру С++ возможно помогут дополнить или скорректировать описанную мной картину. Всех интересующихся приглашаю под кат.


Приведение типов в стиле языка C (C-style cast)


Приведение типов в стиле языка C может привести выражение любого типа к любому другому типу данных (исключение это приведение пользовательских типов по значению, если не определены правила их приведения, а также приведение вещественного типа к указателю или наоборот). К примеру, unsigned int может быть преобразован к указателю на double. Данный метод приведения типов может быть использован в языке C++. Однако, метод приведения типов в стиле языка C не делает проверки типов на совместимость, как это могут сделать static_cast и dynamic_cast на этапе компиляции и на этапе выполнения соответственно. При этом все, что умеют const_cast и reinterpret_cast данный метод приведения типов делать может.

Общий вид приведения:

(new_type)exp

, где new_type – новый тип, к которому приводим, а exp – выражение, которое приводится к новому типу.

Т.к. данный оператор не имеет зарезервированного ключевого слова (например, static_cast) найти все места приведения типов в тексте программы будет не очень удобно, если это потребуется.

Показать пример
#include <iostream>

// Пустые классы только
// для теста приведения
struct foo {
};

struct bar {
};

// Наследники bar
struct baz : bar {
};

struct quux : bar {
};

int main() {
    // Переменные простых типов и указатели на переменные простых типов
    int i = 5;
    double d = 111.222;
    char c = 'a';
    int* pi = &i;
    double* pd = &d;
    const int* pсi = &i;
    void* pv = NULL;

    // Объекты классов
    foo oFoo;
    bar oBar;
    baz oBaz;
    quux oQuux;

    // Указатели на объекты классов
    foo* pFoo = &oFoo;
    bar* pBar = &oBar;
    baz* pBaz = &oBaz;
    quux* pQuux = &oQuux;

    // Приводим явно double к int
    i = (int)d;

    // и наоборот
    d = (double)i;

    // указатель на int к char
    c = (char)pi;

    // char к указателю на void
    pv = (void*)c;

    // указатель на void к указателю на int
    pi = (int*)pv;

    // Снимаем константность const int*
    pi = (int*)pсi;

    // Приводим указатель на объект bar к указателю на объект foo
    // из разных иерархий
    pFoo = (foo*)pBar;

    // Приводим указатель на double к double
    d = (double)*pd; // Ошибка!!!

    // А если наоборот?
    pd = (double*)d; // Ошибка!!!

    // Перемещение из одной иерархии наследования в другую
    pBar = (bar*)pBaz;
    pQuux = (quux*)pBar;

    return 0;
}



const_cast


Оператор приведения const_cast удаляет или добавляет квалификаторы const и volatile с исходного типа данных (простые типы, пользовательские типы, указатели, ссылки). Например, был const int, а после преобразования стал int или наоборот. Квалификаторы const и volatile называют cv-квалификаторы (cv-qualifiers). Данные квалификаторы указываются перед именами типов. Как ни трудно догадаться квалификатор const задает константность, т.е. защищает переменную от изменения. Квалификатор volatile говорит о том, что значение переменной может меняться без явного выполнения присваивания. Это обеспечивает защиту от оптимизации компилятором операций с данной переменной.

Общий вид приведения:

const_cast<new_type>(exp)

Показать пример
#include <iostream>

// Снятие константности
void foo(const int* pc, const int& r) {
    int* p;
    // Сняли константность и записали 33
    p = const_cast<int*>(pc);
    *p = 33;
    // Сняли константность и записали 55
    const_cast<int&>(r) = 55;
}

// Добавление константности
void bar(int* p, int& r) {
    const int* pc;
    // Добавили константность
    // и пытаемся записать 33
    pc= const_cast<const int*>(p);
    *pc = 33; // Ошибка!!!
    // Добавили константность
    // и пытаемся записать 55
    const_cast<const int&>(r) = 55; // Ошибка!!!
}

// Снятие volatile
void baz(volatile int* pv, volatile int& r) {
    int* p;
    // Сняли volatile и записали 33
    p = const_cast<int*>(pv);
    *p = 33;
    // Сняли volatile и записали 55
    const_cast<int&>(r) = 55;
}

// Добавление volatile
void quux(int* p, int& r) {
    volatile int* pv;
    // Добавили volatile и записали 33
    pv = const_cast<volatile int*>(p);
    *pv = 33;
    // Добавили volatile и записали 55
    const_cast<volatile int&>(r) = 55;
}

int main() {
    int x = 3, y = 5;
    std::cout << x << " " << y << std::endl;
    // Снимаем константность
    foo(&x, y);
    std::cout << x << " " << y << std::endl;
    x = 3;
    y = 5;
    // Добавляем константность
    bar(&x, y); // Ошибка!!!
    std::cout << x << " " << y << std::endl;
    // Снимаем volatile
    baz(&x, y);
    std::cout << x << " " << y << std::endl;
    x = 3;
    y = 5;
    std::cout << x << " " << y << std::endl;
    // Добавляем volatile
    quux(&x, y);
    std::cout << x << " " << y << std::endl;
    return 0;
}



Дополнительный пример от пользователя 5nw

Показать пример
#include <iostream>

using namespace std;

void f(int* x) {
    cout << __PRETTY_FUNCTION__ << endl;
}

void f(const int* x) {
    cout << __PRETTY_FUNCTION__ << endl;
}

int main() {
    int x = 5;
    int* px = &x;

    f(px);
    f(const_cast<const int*>(px));

    return 0;
}



Квалификаторы const и volatile можно удалить или добавить только с помощью оператора приведения const_cast и приведения типов в стиле языка C. Другие операторы приведения типов не влияют на квалификаторы const и volatile (reinterpret_cast, static_cast, dynamic_cast).

reinterpret_cast


Оператор приведения reinterpret_cast используется для приведения несовместимых типов. Может приводить целое число к указателю, указатель к целому числу, указатель к указателю (это же касается и ссылок). Является функционально усеченным аналогом приведения типов в стиле языка С. Отличие состоит в том, что reinterpret_cast не может снимать квалификаторы const и volatile, а также не может делать небезопасное приведение типов не через указатели, а напрямую по значению. Например, переменную типа int к переменной типа double привести при помощи reinterpret_cast нельзя.

Общий вид приведения:

reinterpret_cast<new_type>(exp)

Показать пример
#include <iostream>

// Пустые классы только для теста приведения
struct foo {
};

struct bar {
};

// Наследники bar
struct baz : bar {
};

struct quux : bar {
};

int main() {
    // Переменные простых типов и указатели на переменные простых типов
    int i = 5;
    double d = 111.222;
    char c = 'a';
    int* pi = &i;
    double* pd = &d;
    const int* pсi = &i;
    void* pv = nullptr;

    // Объекты классов
    foo oFoo;
    bar oBar;
    baz oBaz;
    quux oQuux;

    // Указатели на объекты классов
    foo* pFoo = &oFoo;
    bar* pBar = &oBar;
    baz* pBaz = &oBaz;
    quux* pQuux = &oQuux;

    // Приводим явно double к int
    i = reinterpret_cast<int>(d); // Ошибка!!!

    // и наоборот
    d = reinterpret_cast<int>(i); // Ошибка!!!

    // указатель на int к char
    c = reinterpret_cast<char>(pi);

    // char к указателю на void
    pv = reinterpret_cast<void*>(c);

    // указатель на void к указателю на int
    pi = reinterpret_cast<int*>(pv);

    // Снимаем константность const int*
    pi = reinterpret_cast<int*>(pсi); // Ошибка!!!

    // Приводим указатель на объект bar к указателю на объект foo
    // из разных иерархий
    pFoo = reinterpret_cast<foo*>(pBar);

    // Приводим указатель на double к double
    d = reinterpret_cast<double>(pd); // Ошибка!!!

    // А если наоборот?
    pd = reinterpret_cast<double*>(d); // Ошибка!!!

    // Перемещение из одной иерархии наследования в другую
    pBar = reinterpret_cast<bar*>(pBaz);
    pQuux = reinterpret_cast<quux*>(pQuux);

    return 0;
}


static_cast


Оператор приведения static_cast применяется для неполиморфного приведения типов на этапе компиляции программы. Отличие static_cast от приведения типов в стиле языка C состоит в том, что данный оператор приведения может отслеживать недопустимые преобразования, такие как приведение указателя к значению или наоборот (unsigned int к указателю на double не приведет), а также приведение указателей и ссылок разных типов считается корректным только, если это приведение вверх или вниз по одной иерархии наследования классов, либо это указатель на void. В случае фиксации отклонения от данных ограничений будет выдана ошибка при компиляции программы. При множественном наследовании static_cast может вернуть указатель не на исходный объект, а на его подобъект.

Общий вид приведения:

static _cast<new_type>(exp)

Показать пример
#include <iostream>

// Пустые классы только для теста приведения
struct foo {
};

struct bar {
};

// Наследники bar
struct baz : bar {
};

struct quux : bar {
};

int main() {
    // Переменные простых типов и указатели на переменные простых типов
    int i = 5;
    double d = 111.222;
    char c = 'a';
    int* pi = &i;
    double* pd = &d;
    const int* pci = &i;
    void* pv = nullptr;

    // Объекты классов
    foo oFoo;
    bar oBar;
    baz oBaz;
    quux oQuux;

    // Указатели на объекты классов
    foo* pFoo = &oFoo;
    bar* pBar = &oBar;
    baz* pBaz = &oBaz;
    quux* pQuux = &oQuux;

    // Приводим явно double к int
    i = static_cast<int>(d);

    // и наоборот
    d = static_cast<int>(i);

    // указатель на int к char
    c = static_cast<char>(*pi); // Ошибка!!!

    // char к указателю на void
    pv = static_cast<void*>(&c); // Ошибка!!!

    // указатель на void к указателю на int
    pi = static_cast<int*>(pv);

    // Снимаем константность const int*
    pi = static_cast<int*>(pci); // Ошибка!!!

    // Приводим указатель на объект bar к указателю на объект foo
    // из разных иерархий
    pFoo = static_cast<foo*>(pBar); // Ошибка!!!

    // Приводим указатель на double к double
    d = static_cast<double>(*pd); // Ошибка!!!

    // А если наоборот?
    pd = static_cast<double*>(d); // Ошибка!!!

    // Перемещение из одной иерархии наследования в другую
    pBar = static_cast<bar*>(pBaz);
    pQuux = static_cast<quux*>(pBar);

    return 0;
}


dynamic_cast


Оператор приведения dynamic_cast применяется для полиморфного приведения типов на этапе выполнения программы (класс считается полиморфным, если в нем есть хотя бы одна виртуальная функция). Если указатель, подлежащий приведению, ссылается на объект результирующего класса или объект класса производный от результирующего то приведение считается успешным. То же самое для ссылок. Если приведение невозможно, то на этапе выполнения программы будет возвращен NULL, если приводятся указатели. Если приведение производится над ссылками, то будет сгенерировано исключение std::bad_cast. Несмотря на то, что dynamic_cast предназначен для приведения полиморфных типов по иерархии наследования, он может быть использован и для обычных неполиморфных типов вверх по иерархии. В этом случае ошибка будет получена на этапе компиляции. Оператор приведения dynamic_cast может приводить указатель на полиморфный тип к указателю на void, но не может приводить указатель на void к другому типу. Способность dynamic_cast приводить полиморфные типы обеспечивается системой RTTI (Run-Time Type Identification), которая позволяет идентифицировать тип объекта в процессе выполнения программы. При множественном наследовании dynamic_cast может вернуть указатель не на исходный объект, а на его подобъект.

Общий вид приведения:

dynamic_cast <new_type>(exp)

Показать пример
#include <iostream>

// Пустые классы только
// для теста приведения
struct foo {
    // Сделали полиморфным
    virtual void do_some() {};
};

struct bar {
    // Сделали полиморфным
    virtual void do_some() {};
};

// Наследники bar
struct baz : bar {
};

struct quux : bar {
};

int main() {
    // Переменные простых типов и указатели на переменные простых типов
    void* pv = nullptr;

    // Объекты классов
    foo oFoo;
    bar oBar;
    baz oBaz;
    quux oQuux;

    // Указатели на объекты классов
    foo* pFoo = &oFoo;
    bar* pBar = &oBar;
    baz* pBaz = &oBaz;
    quux* pQuux = &oQuux;

    // Приводим указатель на объект bar к указателю на объект foo
    // из разных иерархий
    pFoo = dynamic_cast<foo*>(pBar);
    if (pFoo == nullptr) {
        std::cout << "FAIL" << std::endl; // Ошибка на этапе выполнения!!!
    }

    // Приводим указатель на void к указателю на объект bar
    pBar = dynamic_cast<foo*>(pv); // Ошибка на этапе компиляции!!!

    // Приводим указатель на bar к указателю на void
    pv = dynamic_cast<void*>(pBar);

    // Перемещение из одной иерархии наследования в другую
    pBar = dynamic_cast<bar*>(pBaz);
    pQuux = dynamic_cast<quux*>(pBar);
    if (pQuux == nullptr) {
        std::cout << "FAIL" << std::endl; // Ошибка на этапе выполнения!!!
    }

    return 0;
}


Источники:

Видеолекция Евгения Линского с проекта Лекториум
Блог Алёна С++
Этот пост @dreary_eyes
«Полный справочник по C++» Герберт Шилдт
«Дизайн и эволюция языка C++» Бьерн Страуструп

Первоисточник:

Стандарт языка C++ (цена $212)
Бесплатный рабочий проект стандарта языка С++ N3337

Дополнительно:

Изображение взято из поста @SOLON7
Tags:
Hubs:
If this publication inspired you and you want to support the author, do not hesitate to click on the button
+1
Comments37

Articles