Casting of Cpp Shepard-Wang

一 引入

1. casting 会带来的问题

struct region { int size; };

void init_region(char* backing_buffer, size_t buffer_size ) {
    if(buffer_size < current_region.size) {
        LOG(“Buffer size too small”);
        return;
    }
//other init code
}

1.png

为了消除编译器的警告,于是我们做了一番修改:

struct region { int size; };
void init_region(char* backing_buffer, size_t buffer_size ) {
    if(buffer_size < (size_t)current_region.size) {
        LOG(“Buffer size too small”);
        return;
    }
//other init code
}

ok!现在没有警告了!

但是。。。这样做合理吗?或者,这么做安全吗?

一段时间后,如果我们修改代码:

struct region {
    constexpr int INVALID_SIZE = -1;
    int size = INVALID_SIZE;
};
void init_region(char* backing_buffer, size_t buffer_size ) {
    if(buffer_size < (size_t)current_region.size) {
        LOG(“Buffer size too small”);
        return;
    }
//other init code
}

如果此时 current_region.size 的值为 INVALID_SIZE,那么 (size_t)current_region.size 变为 ULONG_MAXunsigned long 类型最大值)。程序会以意想不到的方式失败,或者在日后某个时间点崩溃。

C++ 之父曾经说过,在任何时刻,消除 cast 都会更好

2. 为什么需要 casting

cast 允许我们:

  1. 操作 raw memory
  2. 浏览继承体系(navigate inheritance hierarchy

3. 什么是类型

到底什么是类型?

类型是我们看待内存(比特位)的方式

8 个字节的内存,我们可以将其看为是一个拥有八个字节的字符串;或者是大小为 2 的 int 数组。。。

2.png

二 自动类型转换 (Automatic Type Conversion)

编译器做出的隐式转换

1. 隐式类型转换

cast1.png

最后一个例子中,Float 的构造函数需要去掉 Float

下面两个例子中,不管是两个整形运算或者是比较:如果两个操作数比 int 小,编译器会将两个操作数提升为 int;如果其中一个操作数比另一个操作数小,将小的操作数(int)提升为大的操作数(size_t

这种方式称为整形提升。

2. 算数类型转换 Arithmetic Type Conversion

cast2.png

编译器将两个 unsigned char 的运算转换为 int ,来避免溢出

3. 符号转换 Sign Conversion

cast3.png

4. 用户的转换操作符 User Conversion Operators

类型转换运算符重载

cast4.png

##

三 显式类型转换 (explicit)

1. C 风格 cast

cast5.png

  1. 创建一个 type 类型的临时变量
  2. 通过改变变量中每一个 bits 的含义重写类型系统
  3. 某些情况下可能失败
  4. 可用于常量表达式
  5. 可能造成未定义行为
  6. 参与操作符优先级(3)

随意的类型转换带来混乱

cast6.png

198216 进制为 0x7BE,小端存储模式下,mustang 的内存模型(从低地址开始)为 :BE 07 00 00

经过 prune 函数将 car 类型的 mustang 当作 tree 类型,并将 mustang 低位的 1 个字节置为 false(0),所以 mustang 的内存模型变为:00 07 00 00

0x700 正好是 179216 进制

函数指针

cast7.png

弊端

  1. 语义不明。你是在截断一个变量?还是在创建一个新的变量?在调用函数?还是将所有 bit 的含义彻底改变?
  2. 易错。参考本节第一条(随意转型带来混乱)。
  3. 你不明白这个转型的意义
  4. 不易查找程序中 cast 的地方(Not Grep-able)
  5. 使 C 和 C++ 语法复杂

2. C++ 函数式 cast

  1. 创建临时变量
  2. 内置类型和 C++ 构造函数平等
  3. 只能使用单字的类型名
  4. 参与操作符优先级(2)

cast8.png

template<typename To, typename From>
To convert_to(From& from) { return To(from); }

class A{};
class B{};

int main()
{
    int i = 7;
    float f = convert_to<float>(i);
    A* pa;
    B* pb = convert_to<B*>(pa);
    using astar = A*;
    A* pa2 = astar(pb);
}

3. C++ cast 操作符

a. C++ casting 要解决的问题

  1. 对不同的任务使用不同的符号
  2. 便于辨认和查找
  3. 拥有和 C cast 相同的所有操作
  4. 消除意料之外的错误
  5. 第五点属于心理学范畴 233。因为 C++ 的 cast 非常难写(相比于 ()),所以当你这样写的时候,你对这个转换有十足的把握。

b. static_cast

cast11.png

  1. 创一个 T 类型的临时变量

  2. 通过寻找两个类型之间 隐式类型转换 **, **用户定义的转换(类内转型操作符重载)和 单参构造函数。不能消除 CV 限定符。

使用场景:

  1. 指明 隐式类型转换
  2. 表明 截断
  3. 从派生类向基类转换
  4. void*T* 之间转换
多重转换

cast12.png

cast13.png

  1. 类型 A 拥有一个参数为 int 的单参构造函数
  2. 类型 E 有一个用户定义的 int 类型操作符重载函数
继承

从派生类向基类转换,编译器会根据 基类在派生类对象中的内存布局,修改派生类指针。

cast14.png

struct Base1{ virtual ~Base1() = default; int i; };
struct Base2{ virtual ~Base2() = default; int i; };
struct Derived : public Base1, public Base2
{ int i; };

void cmpAddress(void* p1, void* p2)
{
    if(p1 == p2) cout << "same\n";
    else cout << "different\n";
}


int main()
{
    Derived d;
    Derived* p1 = &d;
    auto p2 = static_cast<Base1*>(p1);
    auto p3 = static_cast<Base2*>(p1);

    cmpAddress(p1, p2);
    cmpAddress(p1, p3);

    // 编译器做了隐式类型转换!
    if(p1 == p3) cout << "same!" << endl;

    cmpAddress((char*)p1 + sizeof(Base1), p3);
}

输出:

same
different
same!
same
static_cast 不是完全可靠的

static_cast 不可用于无关类型的转换。

cast16.png

static_cast 也可以向下转型!

class base1 { int a; };
class base2 { int a; };
class derive : public base1, public base2 {int a;};

int main()
{
    derive d;
    base1* pb1 = &d ;
    base2* pb2 = &d ;

    cout << &d << " " << pb1 << " " << pb2 << endl;

    derive* pd1 = static_cast<derive*>(pb1);
    derive* pd2 = static_cast<derive*>(pb2);

    cout << pd1 << " " << pd2 << endl;

    return 0;
}
// 0xd293dffd54 0xd293dffd54 0xd293dffd58
// 0xd293dffd54 0xd293dffd54

加上虚函数依然可以向下转型。

c. const_cast

  1. 修改 CV 限定符,不修改类型
  2. 不修改原来那个变量的 CV 限定符

cast17.png

int main()
{
    const int i = 7;

    modify(const_cast<int*>(&i));
    cout << i << endl;// 7

    const int j = 7;
    const int* pj = &j;
    modify(const_cast<int*>(pj));
    cout << *pj << endl;// 42

    return 0;
}

其实 ij 的值都被修改了。输出 i 的值没有变化是因为编辑器将 i 的值放入寄存器中,虽然 i 在内存中的值已经被修改,但是由于 i 是 const 类型,所有编译器默认 i 的值不会修改,所以直接输出寄存器中 i 最开始的值。

可以给 i 加上 volatile,让 i 的内存始终可见

成员函数重载 member overload

cast18.png

cast19.png

class my_array
{
public:
    my_array() : _arr("HelloWorld") {}

    const char& operator[](int idx) const
    {
        return _arr[idx];
    }

    char& operator[](int idx)
    {
        return const_cast<char&>(
                const_cast<const my_array&>(*this)[idx]
                );
    }

private:
    char _arr[11];
};

int main()
{
    const my_array arr1;
    my_array arr2;

    cout << arr1[3] << endl;
    //arr1[3] = 10;
    arr2[3] = 'X';
    cout << arr2[3] << endl;
}

d. reinterpret_cast

  1. 用于位的简单重新解释。可以对指针和引用进行任意类型的转换!
  2. 也称为 类型双关(绕开类型系统)
  3. 不能使用 constexpr
  4. 不能移除 cv 限定符
  5. 转换类型之间的大小不需要一样
  6. 对于内存映射功能很有用

cast23.png

可访问私有的基类

cast24.png

类型别名 Type Aliasing

两个类型的内存布局是兼容的时候,这两个类型间是可以转化的。

The act of using the memory of one type as if it were a different type when the memory layouts of the two types are compatible

cast25.png

cast26.png

cast27.png

What is Strict Aliasing and Why do we Care? (github.com)

应用于哈希函数
  • reinterpret_cast 的一个实际用途是在哈希函数中,即,通过让两个不同的值几乎不以相同的索引结尾的方式将值映射到索引。

    #include <iostream>
    using namespace std;
      
    // Returns a hash code based on an address
    unsigned short Hash( void *p ) {
       unsigned int val = reinterpret_cast<unsigned int>( p );
       return ( unsigned short )( val ^ (val >> 16));
    }
    

e. dynamic_cast

  1. 公有继承体系
  2. 指针或引用
  3. 无法移除 cv 限定符
  4. 多态(需要虚函数指针)
  5. 需要 RTTI
  6. 如果类型不相关(继承树中没有通路),指针返回 nullptr,引用抛出 std::bad_cast 异常

cast20.png

cast21.png

dynamic_cast 可能开销很大!

cast22.png

四 C++ 是如何处理 C风格的类型转换

尝试顺序:

  1. const_cast
  2. static_cast
  3. reinterpret_cast

cast28.png

五 其他的转换

1. move

  1. 将左值转为右值
  2. 如果存在 move ctor 则调用,否则调用 copy ctor
  3. 不要返回 std::move(var)。编译器会有 RVO 优化。
  4. 等价于 static_cast<T&&>(val)

cast29.png

2. forward

cast30.png

六 如何选择不同类型的 cast

cast31.png