【C++】近期C++特性进阶学习总结(一)

前言

C++的特性多的数不胜数,语言标准也很多,所以不定期对近期所学的C++知识进行总结,是对自身知识体系检查的良好机会,顺便锻炼一下写博客的文笔

三/五/零之法则

三之法则:如果某个类需要用户定义的析构函数、用户定义的复制构造函数或用户定义的复制赋值运算符,那么它几乎肯定需要全部三者。

五之法则:任何想要移动语义的类必须声明全部五个特殊成员函数 (析构函数、拷贝构造、赋值运算、移动拷贝构造、移动赋值运算)

零之法则:有自定义析构函数、复制/移动构造函数或复制/移动赋值运算符的类应该专门处理所有权
当有意将某个基类用于多态用途时,可能需要将它的析构函数声明为公开的虚函数。由于这会阻拦隐式移动(并弃用隐式复制)的生成,因而必须将各特殊成员函数声明为预置的

class base_of_five_defaults
{
public:
    base_of_five_defaults(const base_of_five_defaults&) = default;
    base_of_five_defaults(base_of_five_defaults&&) = default;
    base_of_five_defaults& operator=(const base_of_five_defaults&) = default;
    base_of_five_defaults& operator=(base_of_five_defaults&&) = default;
    virtual ~base_of_five_defaults() = default;
};

扩展阅读:

来自cppreference:三五法则

CRTP

  • Curiously Recurring Template Pattern(奇异的递归模板模式)

CRTP是指一个类A有一个基类,这个基类是类A本身的模板特化。具有编译时多态的特性
如下例子也可通过vtable实现。拿这个例子,将CRTPvtable实现的动态多态进行对比
虚函数:

内存:每个虚函数一个函数指针
运行时:一次函数指针调用
而 CRTP 静态多态的开销是:
内存:每个模板实例化的 Base 副本
运行时:一个函数指针调用 + static_cast 正在做的任何事情
template <typename T>
struct Base {
  void foo() {
    (static_cast<T*>(this))->foo();
  }
};

struct Derived : public Base<Derived> {
  void foo() {
    cout << "derived foo" << endl;
  }
};

struct AnotherDerived : public Base<AnotherDerived> {
  void foo() {
    cout << "AnotherDerived foo" << endl;
  }
};

template<typename T>
void ProcessFoo(Base<T>* b) {
  b->foo();
}


int main()
{
    Derived d1;
    AnotherDerived d2;
    ProcessFoo(&d1);
    ProcessFoo(&d2);
    return 0;
} 

Output:

derived foo
AnotherDerived foo

扩展阅读:

来自cppreference:CRTP
c++标准中对于CRTP的使用例子:std::enable_shared_from_this(cpp11)std::ranges::view_interface(cpp20)

RAII

  • Resource Acquisition Is Initialization(资源获取即初始化)

将资源的生命周期与对象的生命周期所绑定(构造获取资源/析构释放资源,利用了栈上的变量在离开作用域的时候会析构的特性),c++11后的四大smart_point(shared_ptrunique_ptrweak_ptrauto_ptr(在17中废除))采用了这种思想。

扩展阅读:

一文带你了解智能指针(转载并结合总结)

RTTI

  • Run Time Type Identification(运行时类型识别)
  • c++中RTTI的一些体现typeiddynamic_casttype traits
    具体可以看runtime的库的函数__RTtypeid,rtti把所需的type_info(不同编译器会有所不同)信息放在vtable前,大概也是dynamic_cast要求父类必须有虚函数的原因吧
  • 注意,取虚函数表地址时 **(此处请注意环境在32位和64位下的区别,在32/64位下取对象a(带有虚函数的基类的实例)的首地址(虚函数表地址)有区分,即*(int *)&a*(long *)&a的不同,为避免也可直接,(int*)*(int*)(&classname)替换成(intptr_t*)*(intptr_t*)(&classname))**

typeiddynamic_cast的区别(2022/2/14补充):

  • typeid:带有vtable类型,通过vtable,找到对象的动态类型,然后从那个对象的vtable中提取type_info。和函数调用相比还是很慢的。不带有vtable的类型,返回编译时的静态类型
  • dynamic_cast:如前所述找到type_info,然后判断是否可以转换(根据向下自动推导得到的地址与type_info中的地址比较(不同编译器可能此处判断方式不同)),然后调整指针。运行时成本取决于所涉及的两个类在类层次结构中的相对位置(相当于在类继承树中找)。

class Base {  
public:  
    virtual void f() { cout << "Base::f" << endl; }  
    virtual void g() { cout << "Base::g" << endl; }  
    void h() { cout << "Base::h" << endl; }  
};  
  
typedef void(*Fun)(void);  //函数指针  
int main()  
{  
    Base b;  
    //  这里指针操作比较混乱,在此稍微解析下:  
  
    //  *****printf("虚表地址:%p\n", *(int *)&b); 解析*****:  
    //  1.&b代表对象b的起始地址  
    //  2.(int *)&b 强转成int *类型,为了后面取b对象的前四个字节,前四个字节是虚表指针  
    //  3.*(int *)&b 取前四个字节,即vptr虚表地址  
    //  
  
    //  *****printf("第一个虚函数地址:%p\n", *(int *)*(int *)&b);*****:  
    //  根据上面的解析我们知道*(int *)&b是vptr,即虚表指针.并且虚表是存放虚函数指针的  
    //  所以虚表中每个元素(虚函数指针)在32位编译器下是4个字节,因此(int *)*(int *)&b  
    //  这样强转后为了后面的取四个字节.所以*(int *)*(int *)&b就是虚表的第一个元素.  
    //  即f()的地址.  
    //  那么接下来的取第二个虚函数地址也就依次类推.  始终记着vptr指向的是一块内存,  
    //  这块内存存放着虚函数地址,这块内存就是我们所说的虚表.  
    //  
    printf("虚表地址:%p\n", *(int *)&b);  
    printf("第一个虚函数地址:%p\n", *(int *)*(int *)&b);  
    printf("第二个虚函数地址:%p\n", *((int *)*(int *)(&b) + 1));  
  
    Fun pfun = (Fun)*((int *)*(int *)(&b));  //vitural f();  
    printf("f():%p\n", pfun);  
    pfun();  
  
    pfun = (Fun)(*((int *)*(int *)(&b) + 1));  //vitural g();  
    printf("g():%p\n", pfun);  
    pfun();  
  
}

扩展阅读:

C++ RTTI 实现原理详解

(C++对象模型):RTTI运行时类型识别回顾与存储位置介绍

【专业技术】C++ RTTI及“反射”技术
C++ 虚函数 获取C++虚表地址和虚函数地址

RTTR

  • 反射是一个进程检查、反省和修改其自身结构和行为的能力
  • Run Time Type Reflection(运行时类型反射)
    众所周知,java、c#、Go等语言在语言层面支持了反射特性。而c++不支持反射,因为C++没有在语言层面提供返回类的metadata的能力,所以很多属性要靠手动注册,于是乎有人自造轮子搞了个反射机制(UE中的U++通过UHT和UBT来支持反射)

扩展阅读:

Run Time Type Reflection
C++ Reflection Library
U大佬的static reflection library
U大佬的dynamic reflection library

auto接收std::vector<bool>::reference的问题

注意此处的BoolData类型是std::vector<bool>::reference,此处是历史遗留问题,设计std::vector<bool>的时候,认为bool只需要1bit,内部做了内存优化,所以用[]访问的时候,得到的是一个内部(被压了位)对象的引用
如果在长度确定的情况下,用std::bitset代替std::vector<bool>是一个更好地选择

    std::vector<bool> BoolDatas;

    // BoolData: std::vector<bool>::reference
    for (auto BoolData : BoolDatas)
    {
    }
    
    // IntData: int
    std::vector<int> IntDatas;
    for (auto IntData : IntDatas)
    {
    }

扩展阅读:

cppreference: std::vector<bool>::reference

类型擦除

将原有类型消除或者隐藏,换言之,在封装接口中,很多情况下我不关心具体类型是什么或者根本不需要这个类型,它可以使接口有更好的通用性、延展性,消除耦合,减少重复代码

  • 一个很详细关于类型擦除的介绍:类型擦除,从多态、template、std::varient(来自boost::varient)、std::any(来自boost::any)、到closesure去分析

扩展阅读:

类型擦除

boost

只能说boost yyds啊,除了模板多,多次编译会导致编译时间长以外,功能真的很强大 确实如其名boost。例如c++17中的std::filesystemstd::anystd::varient直接来自于boost中。还有boost::program_options用于处理控制台的输入参数也是很方便

#、#@、##、__VA_ARGS__ 应用

#define Conn(x,y)  x##y // 表示x连接y
#define ToChar(x)  #@x  // 给x加上单引号
#define ToString(x) #x  // 给x加上双引号

#

char* str = ToString(123132);     // str="123132";

##

int n = Conn(123,456);            //n=123456;
char* str = Conn("asdf", "add")   //str = "asdfadf";

也可用来省略可变参数为空时,去掉前面的,

#define ESC_START     "\033["
#define ESC_END       "\033[0m"
#define COLOR_FATAL   "31;40;5m"
#define COLOR_ALERT   "31;40;1m"
#define COLOR_CRIT    "31;40;1m"
#define COLOR_ERROR   "31;40;1m"
#define COLOR_WARN    "33;40;1m"
#define COLOR_NOTICE  "34;40;1m"
#define COLOR_INFO    "32;40;1m"
#define COLOR_DEBUG   "36;40;1m"
#define COLOR_TRACE   "37;40;1m"

#define Msg_Info(format, ...) (printf( ESC_START COLOR_INFO "[INFO]-[%s]-[%s]-[%d]:" format ESC_END, __FILE__, __FUNCTION__ , __LINE__, ##__VA_ARGS__))
#define Msg_Debug(format, ...) (printf( ESC_START COLOR_DEBUG "[DEBUG]-[%s]-[%s]-[%d]:" format ESC_END, __FILE__, __FUNCTION__ , __LINE__, ##__VA_ARGS__))
#define Msg_Warn(format, ...) (printf( ESC_START COLOR_WARN "[WARN]-[%s]-[%s]-[%d]:" format ESC_END, __FILE__, __FUNCTION__ , __LINE__, ##__VA_ARGS__))
#define Msg_Error(format, ...) (printf( ESC_START COLOR_ERROR "[ERROR]-[%s]-[%s]-[%d]:" format ESC_END, __FILE__, __FUNCTION__ , __LINE__, ##__VA_ARGS__))

int main()
{
  Msg_Info("test!\n");
  Msg_Warn("%d\n", 10);
  Msg_Error("%s\n", "error");
  Msg_Debug("Debug\n");
  // 当可变参数为空时
  Msg_Debug();

  /*
  (printf( "\033[" "32;40;1m" "[INFO]-[%s]-[%s]-[%d]:" "test!\n" "\033[0m", "D:\\repos\\C++Project\\main.cpp",  __FUNCTION__   , 66 ));
  (printf( "\033[" "33;40;1m" "[WARN]-[%s]-[%s]-[%d]:" "%d\n" "\033[0m", "D:\\repos\\C++Project\\main.cpp",  __FUNCTION__   , 67,10));
  (printf( "\033[" "31;40;1m" "[ERROR]-[%s]-[%s]-[%d]:" "%s\n" "\033[0m", "D:\\repos\\C++Project\\main.cpp",  __FUNCTION__   , 68,"error"));
  (printf( "\033[" "36;40;1m" "[DEBUG]-[%s]-[%s]-[%d]:" "Debug\n" "\033[0m", "D:\\repos\\C++Project\\main.cpp",  __FUNCTION__   , 69 ));
  (printf( "\033[" "36;40;1m" "[DEBUG]-[%s]-[%s]-[%d]:"  "\033[0m", "D:\\repos\\C++Project\\main.cpp",  __FUNCTION__   , 77 ));
  */
}

#@

char a = ToChar(1);               // a='1';
// char a = ToChar(123);          // 编译器报错

_VA_ARGS_

  • 用于宏定义中代表可变参数
#define debug(...) printf(__VA_ARGS__)

c++20 初始化表达式

  • 使用c++11的range for的时候,就在好奇为什么没带有Initializationrange for,终于在C++20中见到了
for (Initialization ; traverse data)
{
    // dosomething()
}

不能有const_cast<>(Data)的原因

用const_cast<T>时,特别想有自动推导参数类型 然后 转成对应的没有const版本,但找了一下stackoverflow关于此的讨论,在用const_cast时,需考虑转换后的需求。如以下代码做举例

class Base{
  // 对数据层封装,将函数修饰成const防止修改成员数据
  const Data& GetData() const { return data; }
private:
  Data data;
}

假设有std::unique_ptr<Base> b,那么我们通过以下方式获取数据

main()
{
  // const Data& 类型
  auto& Data = b->GetData();
}

但难免之后的业务需求起来后,上层逻辑调用获取这个Data时有可能会修改数据
提供非Const版本,当数据层中过多数据均是如此时,情况会非常糟糕

  Data& GetNConstData() { return data; }

于是能想到const_cast,转换后可以直接修改对应数据

  auto& NConstData = const_cast<Data&>(b->GetData());

问题又来了,过多的写转换类型过于繁琐,于是乎有人写如下代码尝试减少重复代码

  const auto& CData = b.GetData();
  // decltype自动推导类型 std::remove_const_t<T> 返回T被移除const后的type
  // using T = const Data&
  using T = typename std::remove_const_t<decltype(CData)>;
  // 很遗憾,经const_cast转换后仍是const类型
  auto& it = const_cast<T&>(CData);        // const Data& 
  auto it2 = const_cast<T&>(CData);        // Data

于是就会疑问为什么标准库不提供类似const_cast<>(Data)的接口,通过自动推断类型然后取非const版本作为模板类型
后面翻阅了一下stackoverflow,高赞解释如下

  • 如果允许某种模板推导,它会更容易发生意外错误。其次const_cast也可以用来删除volatile,编译器怎么知道你想扔掉什么呢

总结

在过去半年内,个人比较热爱C++的各种奇淫特性,内容更偏向笔记时所记录,所以本文更偏向简约不详细深入。不对某个特性进行深入总结,宗旨在抛砖引玉,简单地介绍特性的作用和用法,再通过后面的我觉得可以阅读的扩展阅读可进行深入了解。

TODO

  • C++进阶学习总结(二):
    • POD
    • CTAD和折叠表达式
    • type_traits
    • C++17一些值得了解的特性
    • 模板(SFINAE,std::enable_if(c++11),concept (c++20))
posted @ 2022-02-11 13:31  shadow_lr  阅读(676)  评论(5编辑  收藏  举报