C++ 核心指南(C++ Core Guidelines)是由 Bjarne Stroustrup、Herb Sutter 等頂尖 C++ 專家創建的一份 C++ 指南、規則及最佳實踐,旨在幫助大家正確、高效地使用“現代 C++”,
這份指南側重于介面、資源管理、記憶體管理、并發等 High-level 主題,遵循這些規則可以最大程度地保證靜態型別安全,避免資源泄露及常見的錯誤,使得程式運行得更快、更好,
R. Resource Management
本章的基本目標是:不產生資源泄漏,不持有不再需要的資源,資源是指需要申請和(顯式或隱式)釋放的任何東西,如記憶體、檔案句柄、套接字、鎖等,如果一個物體“擁有”資源,則意味著該物體負責釋放資源,在個別場景下,資源泄露是可以接受的,例如有足夠的記憶體處理最大的輸入,或者出于性能優化考慮,只申請不釋放,本章的規則不適用于這類特殊情況,
- 資源管理規則總結
- R.1: 通過資源句柄和 RAII 自動管理資源
- R.2: 在介面中,裸指標只用來表示單個物件
- R.3: 裸指標(T*)不擁有資源
- R.4: 裸參考(T&)不擁有資源
- R.5: 優先使用物件,非必要不在堆上分配
- R.6: 避免使用非 const 的全域變數
R.1: 通過資源句柄和 RAII 自動管理資源
避免泄露以及手動管理資源的復雜性,C++ 的構造/析構反映了資源獲取/釋放的固有對稱性,如 fopen/fclose,lock/unlock,new/delete,在處理資源時,如果需要通過一對函式獲取/釋放資源,則可以把該資源封裝到一個類中:在構造中獲取資源,析構中釋放資源,
反面例子
void send(X* x, string_view destination)
{
auto port = open_port(destination);
my_mutex.lock();
// ...
send(port, x);
// ...
my_mutex.unlock();
close_port(port);
delete x;
}
在這個例子中,必須記得在所有路徑上呼叫 unlock、close_port、delete,并且要保證剛好呼叫一次,并且一旦 ... 部分拋出例外,就會導致資源 x 泄露,my_mutex 也一直是 locked 狀態,
正面例子
void send(unique_ptr<X> x, string_view destination)
{
Port port{destination}; // port 擁有 PortHandle
lock_guard<mutex> guard{my_mutex}; // guard 擁有鎖
// ...
send(port, x);
// ...
} // 自動 unlock my_mutex 并且 delete x
現在所有資源都是自動釋放的,并且保證在所有路徑上,無論是否有例外拋出,都只釋放一次,不僅如此,這個函式也明確宣布了它對指標 x 的所有權,
Port 只是一個簡單的 wrapper,封裝了資源:
class Port {
PortHandle port;
public:
Port(string_view destination) : port{open_port(destination)} { }
~Port() { close_port(port); }
operator PortHandle() { return port; }
// port 句柄通常不能復制,如果有必要,禁用拷貝構造和拷貝賦值運算子
Port(const Port&) = delete;
Port& operator=(const Port&) = delete;
};
注
如果資源不是以“具有解構式的類”的方式呈現,就用類封裝一下,或者使用 GSL 庫中的 finally ,以便資源能夠自動釋放
相關慣用法
RAII
R.2: 在介面中,裸指標只用來表示單個物件
換句話說,不要用裸指標表示一個陣列,陣列最好用容器型別(如:擁有資源的用 vector、不擁有資源的用 span),容器、視圖包含大小資訊,可以進行邊界檢查,
反面例子
void f(int* p, int n) // n 是 p[] 元素的數量
{
// ...
p[2] = 7; // bad: 裸指標下標
// ...
}
編譯器又不看注釋,如果不看其他代碼,你無法確定 p 是否真的指向 n 個元素,這種情況最好用 span,
例子
void g(int* p, int fmt) // 用 #fmt 格式列印 *p
{
// ... 僅使用 *p 和 p[0] ...
}
例外
C 風格字串通過單個指標(指向以 \0 結尾的字符序列)傳遞,如果你依賴這種約定,最好使用 zstring,而不是 char*
注
很多指向單一元素的指標都可以用參考替代,除非指標可能是 nullptr
代碼檢查建議
- 如果一個指標不是容器、視圖或迭代器,且對該指標進行了算術運算(包括
++),則標記該指標上的算術運算,如果將該規則應用在舊的代碼上,可能產生大量誤報 - 標記作為簡單指標傳遞的陣列
R.3: 裸指標(T*)不擁有資源
在絕大多數的代碼中,裸指標都是“非擁有”(non-owning)的,即指標不擁有資源,使用該指標的代碼不負責釋放指標指向的資源,
我們希望能夠標識出“擁有指標”(owning pointer),這樣就能安全地釋放“擁有指標”指向的資源,
例子
void f()
{
int* p1 = new int{7}; // bad: 裸“擁有指標”,f() 要負責釋放 p1 指向的資源
auto p2 = make_unique<int>(7); // OK: unique_ptr 擁有 int
// ...
}
unique_ptr 可以保證及時釋放資源,即使發生例外也不會產生泄漏,但是 T* 無法保證,
例子
template<typename T>
class X {
public:
T* p; // bad: 不清楚 p 是否是“擁有指標”
T* q; // bad: 不清楚 q 是否是“擁有指標”
// ...
};
上面的代碼無法知道 p、q 是否是“擁有指標”,可以通過下面的方式明確所有權:
template<typename T>
class X2 {
public:
owner<T*> p; // OK: p 是“擁有指標”
T* q; // OK: q 不是“擁有指標”
// ...
};
注
owner<T*> 本質上也是 T*,用 owner<T*> 替代 T* 不會影響 ABI 及原來的代碼,它只是用來提示程式員和代碼分析工具,如果 owner<T*> 是類的成員,通常應該在類的析構中釋放指向的資源,
例外
主要是歷史遺留代碼,尤其是那些要需要和 C 或者和 C 風格 C++ ABI 兼容,我們不能把所有的“擁有指標”轉換成 unique_ptr 或 shared_ptr 來解決這個問題,部分原因是我們在基本的資源管理代碼中需要/使用了裸“擁有指標”,例如常見的 vector 實作中,有一個“擁有指標”和兩個“非擁有指標”,許多 ABI(以及所有 C 代碼介面)都還使用 T*,其中有些是“擁有指標”,有些介面不能簡單地用 owner<T*> 標注,因為要和 C 兼容(這個問題可以用宏來解決,只在 C++ 時展開宏,這是宏少有的正確使用場景),
反面例子
回傳裸指標使得呼叫者無法確切知道該如何進行生命周期管理:呼叫者是否需要釋放指標指向的物件?
Gadget* make_gadget(int n)
{
auto p = new Gadget{n};
// ...
return p;
}
void caller(int n)
{
auto p = make_gadget(n); // 呼叫者要記得洗掉 p
// ...
delete p;
}
上述代碼除了泄漏問題之外,還可能增加不必要的分配/釋放操作,如果 Gadget 很小或者移動成本很低,可以直接按值回傳(詳見 R.5):
Gadget make_gadget(int n)
{
Gadget g{n};
// ...
return g;
}
注
本規則適用于工廠方法
注
在必須使用指標的情況下(比如涉及到多型,回傳基類指標),回傳智能指標
代碼檢查建議
- 如果 delete 了一個不是
owner<T>的裸指標,給出警告 - 如果沒有在所有路徑上 reset 或 delete 某個
owner<T>,給出警告 - 如果 new 回傳的結果賦給一個裸指標,給出警告
- 如果函式回傳了一個在函式內分配的物件,且該物件有移動構造,給出警告,建議考慮按值回傳
R.4: 裸參考(T&)不擁有資源
和 R.3 一樣,大多數的裸參考也不擁有資源,
例子
void f()
{
int& r = *new int{7}; // bad: 裸“擁有參考”
// ...
delete &r; // bad: 洗掉裸指標,違反 R.3 規則
}
R.5: 優先使用作用域物件,非必要不在堆上分配
作用域物件(scoped object)可以是區域物件、全域物件或者類成員,除了作用域物件本身,沒有額外的分配/釋放的開銷,作用域物件內部的成員的生命周期由作用域物件的構造/析構管理,
例子
下面代碼存在的問題:1. 不必要的分配/釋放;2. 如果 ... 部分拋例外,會導致記憶體泄漏;3. 代碼冗長
void f(int n)
{
auto p = new Gadget{n};
// ...
delete p;
}
改用區域物件則可解決上述 3 個問題:
void f(int n)
{
Gadget g{n};
// ...
}
代碼檢查建議
- 如果一個函式內,某個物件在所有路徑上都是先分配,再釋放,給出警告,建議用區域堆疊物件,
- 如果一個區域非 const 的 unique_ptr 或者 shared_ptr 在其生命周期結束之前沒有被 move、拷貝、重新賦值或者
reset,給出警告- 例外:如果是一個指向動態陣列的區域 unique_ptr,不給出警告,見下面例子:
例外
可以創建一個堆上分配緩沖區的 const unique_ptr<T[]>,這是表示動態陣列的合法方式
例子
int get_median_value(const std::list<int>& integers)
{
const auto size = integers.size();
// OK: 宣告一個區域 unique_ptr<T[]>.
const auto local_buffer = std::make_unique_for_overwrite<int[]>(size);
std::copy_n(begin(integers), size, local_buffer.get());
std::nth_element(local_buffer.get(), local_buffer.get() + size/2, local_buffer.get() + size);
return local_buffer[size/2];
}
R.6: 避免使用非 const 的全域變數
(同規則 I.2)非 const 的全域變數隱藏了依賴,容易受到無法預測的改變,
例子
struct Data {
// ... lots of stuff ...
} data; // non-const data
void compute() // don't
{
// ... use data ...
}
void output() // don't
{
// ... use data ...
}
你很難知道是否還會有其他地方改變 data
警告
全域物件的初始化順序不能完全確定,所以只能用常量去初始化全域物件,另外,即使是 const 物件,其初始化順序也可能是未定義的,
例外
即便是全域物件,一般來說也比單例好
注
-
全域常量是有用的
-
本規則除了適用于“全域變數”外,對命名空間里的變數也適用
替代方案
如果是出于避免拷貝的目的而使用全域變數,可以考慮用 reference to const 來傳遞資料,或者把該資料作為物件的狀態(即類的成員),然后通過成員函式是來操作該成員,
警告
小心資料爭用(data race):如果一個執行緒可以訪問 non-local 資料(或者參考傳遞的資料),同時另一個執行緒執行 callee,就可能產生 data race,每個指向可變資料的指標或參考都可能產生資料爭用
注
不可變資料(immutable data)不會產生靜態條件(race condition)
注
這條規則是“盡量避免”,不是“禁止使用”,會有一些(極少數)例外,比如 cin、cout 和 cerr
代碼檢查建議
報告所有在命名空間/全域中宣告的非 const 變數及指向非 const 物件的指標或參考
轉載請註明出處,本文鏈接:https://www.uj5u.com/qita/555863.html
標籤:其他
上一篇:空 - 三眼烏鴉
下一篇:返回列表
