前言
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實現。拿這個例子,將CRTP與vtable實現的動態多態進行對比
虛函數:
內存:每個虛函數一個函數指針而 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_ptr、unique_ptr、weak_ptr、auto_ptr(在17中廢除))采用了這種思想。
擴展閱讀:
RTTI
- Run Time Type Identification(運行時類型識別)
- c++中RTTI的一些體現
typeid、dynamic_cast、type 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))**
typeid和dynamic_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++ 虛函數 獲取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::filesystem、std::any、std::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的時候,就在好奇為什么沒帶有Initialization的range 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))
