【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))


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM