C++中的Named-conversion

Last Updated: 2024-01-10 13:18:26 Wednesday

-- TOC --

C++有4个自带的Named-conversion,语法与实例化函数模板一样,它们是编译器提供的builtin接口。

C语言是没有这样东西的,C语言有implicit conversion(在用等号=赋值或function call时)和forced(explicit) conversion,它们依然可以在C++中使用。

C/C++的类型,只在编译期间有作用,编译后得到的二进制文件,是没有类型信息的。但C++因为要支持运行时多态,提供了RTTI机制(运行时类型检测),还不懂。

为什么要用Named-conversion?(来自一本教材的观点)

The syntax of the C++ explicit casts is intentionally ugly and verbose. This calls attention to a point in the code where the rigid rules of the type system are being bent or broken. The C-style cast doesn’t do this. In addition, it’s not clear from the cast what kind of conversion the programmer is intending. When you use finer instruments like the named casts, the compiler can at least enforce some constraints.

既然是写C++代码,还是建议都使用named conversion!

const_cast

In particular, only const_cast may be used to cast away (remove) constness or volatility. ** 只有const_cast可用来去掉const和volatile修饰,即cv qualifier。除了const或 volatile属性之外,目标类型必须与源类型相同**。const_cast用来操作对象的 cv 属性,可以加上 cv 属性,也可以去掉 cv 属性。

const_cast只能用于指针引用

看下面这种错误的情况:(拿掉const申明)

#include <iostream>
using namespace std;

void do_sth_with_c_string(char *str) {
  // do ...
  cout << str << endl;
}

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

do_sth_with_c_string接口对参数没有const要求,这意味着在其内部,可能存在修改入参的可能性,如果强转一个const类型到接口内,可能UB...

如果反过来增加const一直是OK的:

#include <iostream>
using namespace std;

void print_c_string(const char *str){
    cout << str << endl;
}

int main(){
    char a[] = "abcdefg";
    print_c_string(a);
    print_c_string(const_cast<const char*>(a));  // same
    return 0;
}

遇到一个使用场景

在C++代码中调用ffmpeg的接口,avcodec_find_encoder_by_name接口返回一个const AVCodec*类型的指针,我的AVCodec指针是一个class member,如果将这个member申明为const,那么就只能在initialization list中进行初始化,但find接口有可能会返回错误,需要处理这种错误。因此,简单的修改,直接将find接口的返回用const_cast<AVCodec*>处理一下,但不能对返回值做任何修改。

if ((codec = const_cast<AVCodec*>(avcodec_find_encoder_by_name(name.c_str()))) == nullptr)
        throw string{ "named-encoder could not be found" };

完美的修改,是将codec这个class member去掉,用一个局部变量代替,因为其它流程的代码,用不到这个codec变量了。

诡异的测试结果,可能都是UB

#include <iostream>
using namespace std;

void func(const int &a){
    int &b {const_cast<int&>(a)};
    ++b;
    cout << a << endl;
}

void cfashion(const int &a){
    ++*(int*)&a;
    cout << a << endl;
}

int main(){
    func(1024); // 1025
    cfashion(1024); // 1025
    return 0;
}

在C语言笔记中,有一篇总结const用法的文章,也有上述C风格的用法,但确没有+1的效果。

cppreference上的示例代码

int i = 3;                 // i is not declared const
const int& rci = i;
const_cast<int&>(rci) = 4; // OK: modifies i

一个非const变量,有可能需要在某一段代码中,增加const属性,这个场景很OK。

const int j = 3; // j is declared const
[[maybe_unused]]
int* pj = const_cast<int*>(&j);
// *pj = 4;      // undefined behavior

reinterpret_cast

reinterpret_cast不用用来cast away const and volatile。

这是最容易理解的一个named-conversion,对应的是C语言中,将一个指向某种类型的指针,强转为指向另一种类型的指针,地址还是一样,只是对内容的解释可以不一样了。

#include <iostream>
#include <vector>
using namespace std;

int main(void) {
    int a{0x31323334};
    int* b{&a};
    char* c{reinterpret_cast<char*>(b)};
    cout << *c << *(c+1) << *(c+2) << *(c+3) << endl;
    return 0;
}

在C语言中,我们这样做reinterpret_cast,一样的:

int a = 123;
char *pa = (char*)&a;

static_cast

static_cast用来完成basic type conversion,它不用能用来cast away const and volatile。

static_cast,编译时的类型转换,转换失败会有编译告警。

static_cast can perform conversions between pointers to related classes, not only upcasts (from pointer-to-derived to pointer-to-base), but also downcasts (from pointer-to-base to pointer-to-derived). No checks are performed during runtime to guarantee that the object being converted is in fact a full object of the destination type. Therefore, it is up to the programmer to ensure that the conversion is safe. On the other side, it does not incur the overhead of the type-safety checks of dynamic_cast.

static_cast is also able to perform all conversions allowed implicitly (not only those with pointers to classes)

所有的允许的隐式类型转换,都可以使用static_cast来“显式地”表达出来。

在C语言中,我们这样做static_cast,一样的:

long int a = 123456;
int b = (int)a;  // might be narrowed

实现std::move的功能:

#include <iostream>
#include <string>
using namespace std;


template<typename T>
T&& mymove(T &t){
    return static_cast<T&&>(t);
    // return (T&&)(t);  is still working... E...
}


int main(){
    string a {"abcde"};
    string b { mymove(a) };
    // a已空,仅输出 a:
    cout << "a:" << a << endl;
    // 输出 b:abcde
    cout << "b:" << b << endl;
    return 0;
}

static_cast更安全

因为它会做一些类型检查,只有相关的类型才能够转换

float a {1.23};
int *b {static_cast<int*>(&a)};      // error
int *b {(int*)&a};                   // no error
int *b {reinterpret_cast<int*>(&a)}; // no error

static_cast不能去掉cv

int i = 123;
const int *a = &i;
int *b = static_cast<int*>(a);
// compile error:
error: invalid ‘static_cast’ from type ‘const int*’ to type ‘int*’
    9 |     int *b = static_cast<int*>(a);
int i = 123;
volatile int *a = &i;
int *b = static_cast<int*>(a);
// compile error:
error: invalid ‘static_cast’ from type ‘volatile int*’ to type ‘int*’
    9 |     int *b = static_cast<int*>(a);

dynamic_cast

dynamic_cast只能用于指向类对象的指针或引用。

dynamic_cast是四个强制类型转换操作符中最特殊的一个,它支持运行时识别指针或引用(这就是dynamic的含义,static_cast就是编译时啦)。dynamic_cast依赖于RTTI信息(Run Time Type Information,可能需要编译器打开开关),在转换时,dynamic_cast会检查转换的source对象是否真的可以转换成target类型,这种检查不是语法上的,而是运行时检查。

dynamic_cast主要用于类继承层次间的指针或引用安全地向下转换,向上转换无需使用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 == nullptr)
            cout << "nullptr pointer on first type-cast.\n";

        pd = dynamic_cast<Derived*>(pbb);
        if (pd == nullptr)
            cout << "nullptr pointer on second type-cast.\n";
    } catch (exception& e) {
        cout << "Exception: " << e.what();
    }
    return 0;
}

输出:

nullptr pointer on second type-cast.

pbb指向的内存是基类对象,不能转成派生类对象,因此dynamic_cast转换失败。

自定义cast

在教材上看到一个示例,用static_cast实现预防narrow cast的template函数,我稍微修改了一下下:

#include <stdexcept>

template <typename To, typename From>
To no_narrow_cast(From value) {
    const auto converted = static_cast<To>(value);
    const auto backwards = static_cast<From>(converted);
    if (value != backwards)
        throw std::runtime_error{ "Narrowed!" };
    return converted;
}

这个template函数,拥有了运行时的是否出现了narrow cast的检查。(应用范围也就限于primitive types)

本文链接:https://cs.pynote.net/sf/c/cpp/202210051/

-- EOF --

-- MORE --