本節(jié)中的規(guī)則非?;\統(tǒng)。
哲學規(guī)則總結:
- P.1:直接在代碼中表達想法
- P.2:用ISO標準C ++編寫代碼
- P.3:表達意圖
- P.4:理想情況下,程序應該是靜態(tài)類型安全的
- P.5:與運行時檢查相比,更喜歡編譯時檢查
- P.6:在編譯時無法檢查的內容應該在運行時檢查
- P.7:盡早發(fā)現(xiàn)運行時錯誤
- P.8:不要泄漏任何資源
- P.9:不要浪費時間或空間
- P.10: 更喜歡不可變數(shù)據(jù)而不是可變數(shù)據(jù)
- P.11:封裝凌亂的結構,而不是遍布代碼
- P.12:適當使用支持工具
- P.13:根據(jù)需要使用支持庫
哲學規(guī)則通常不是機械式的檢查, 然而,個人規(guī)則反映了這些哲學的主題。 沒有哲學基礎,更具體/明確/可檢查的規(guī)則就缺乏基本原理。
P.1:直接在代碼中表達想法
Reason
編譯器不讀取注釋(或設計文檔),也不會讀取許多程序員。 代碼中表達的內容定義了語義,并且(原則上)可以由編譯器和其他工具檢查。
Example
class Date {
// ...
public:
Month month() const; // do
int month(); // don't
// ...
};
month的第一個聲明明確是關于返回a month而不是修改Date對象的狀態(tài)。 第二個版本讓讀者猜測并為未捕獲的錯誤打開更多可能性。
Example; bad
這個循環(huán)是std :: find的限制形式:
void f(vector<string>& v)
{
string val;
cin >> val;
// ...
int index = -1; // bad, plus should use gsl::index
for (int i = 0; i < v.size(); ++i) {
if (v[i] == val) {
index = i;
break;
}
}
// ...
}
Example; good
意圖更清晰的表達應該是:
void f(vector<string>& v)
{
string val;
cin >> val;
// ...
auto p = find(begin(v), end(v), val); // better
// ...
}
一個設計良好的庫表達意圖(what is to be done, rather than just how something is being done)遠比直接使用語言功能更好。
C ++程序員應該了解標準庫的基礎知識,并在適當?shù)牡胤绞褂盟?任何程序員都應該了解正在做的項目的基礎庫的知識,并在適當?shù)胤绞褂盟鼈儭?任何了解過這些指南的程序員都應該知道指南支持庫,適時使用它在自己工作中或者學習當中。
Example
change_speed(double s); // bad: what does s signify?
// ...
change_speed(2.3);
一個好方法是明確雙重的意義(new speed or delta on old speed?) 和使用的單位:
change_speed(Speed s); // better: the meaning of s is specified
// ...
change_speed(2.3); // error: no unit
change_speed(23m / 10s); // meters per second
我們本可以接受一個普通 (unit-less) double as a delta,但這很容易出錯。 如果我們想要絕對速度和增量,我們就會定義一個Delta類型。
Enforcement
一般來說很難。
- 習慣性使用const(檢查成員函數(shù)是否修改其對象;檢查函數(shù)是否修改指針或引用傳遞的參數(shù))
- cast是個標記((強制類型轉換中和類型系統(tǒng))
- 檢測模仿標準庫的代碼(hard)
P.2:用ISO標準C ++編寫代碼
Reason
這是一套編寫ISO標準C ++的指南。
Note
存在需要擴展的環(huán)境,例如,訪問系統(tǒng)資源。 在這種情況下,本地化使用必要的擴展并使用非核心編碼指南控制它們的使用。 如果可能,構建封裝擴展的接口,以便可以在不支持這些擴展的系統(tǒng)上關閉或編譯它們。
擴展通常沒有嚴格定義的語義。 即使是多個編譯器常見且由多個編譯器實現(xiàn)的擴展,也可能具有略微不同的行為和邊緣情況行為,這是由于沒有嚴格的標準定義。 充分利用任何此類擴展后,預計可移植性將受到影響。
Note
使用有效的ISO C ++并不能保證可移植性(更不用說正確性)了。 避免依賴于未定義的行為(e.g., undefined order of evaluation)并且注意具有實現(xiàn)定義含義的構造(e.g., sizeof(int))。
Note
存在必須限制使用標準C ++語言或庫特征的環(huán)境,例如,以避免飛行器控制軟件標準所要求的動態(tài)存儲器分配。 在這種情況下,通過針對特定環(huán)境定制的這些編碼指南的擴展來控制它們的(dis)使用。
Enforcement
使用最新的C ++編譯器(當前為C ++ 17,C ++ 14或C ++ 11),其中包含一組不接受擴展的選項。
P.3:表達意圖
Reason
除非聲明某些代碼的意圖(例如,在名稱或注釋中),否則無法判斷代碼是否完成了應該執(zhí)行的操作。
Example
gsl::index i = 0;
while (i < v.size()) {
// ... do something with v[i] ...
}
這里沒有表達“僅僅”循環(huán)v元素的意圖。 索引的實現(xiàn)細節(jié)被公開(因此可能被濫用),并且i比循環(huán)的范圍更長,這可能是也可能不是。 讀者無法從這部分代碼中了解到。
Better:
for (const auto& x : v) { /* do something with the value of x */ }
現(xiàn)在,沒有明確提到迭代機制,并且循環(huán)操作對const元素的引用,以便不會發(fā)生意外修改。 如果需要修改,請說:
for (auto& x : v) { /* modify x */ }
有關for語句的更多詳細信息,請參閱ES.71。 有時更好,使用命名算法:
for_each(v, [](int x) { /* do something with the value of x */ });
for_each(par, v, [](int x) { /* do something with the value of x */ });
最后一個變量清楚地表明我們對v的元素的處理順序不感興趣。
程序員應該熟悉:
- The guidelines support library
- The ISO C++ Standard Library
- 無論什么基礎庫都可用于當前項目
Note
替代表述:說出應該做什么,而不僅僅是應該如何做。
Note
一些語言結構比其他語言結構表達意圖更好
Example
如果兩個ints意味著是2D點的坐標,那么說:
draw_line(int, int, int, int); // obscure
draw_line(Point, Point); // clearer
Enforcement
尋找有更好選擇的常見模式:
- simple
forloops vs.range-forloops
+f(T*, int)interfaces vs.f(span<T>)interfaces - 循環(huán)變量范圍太大
- 赤裸裸的new和delete
- 具有許多內置類型參數(shù)的函數(shù)
這里是聰明和高級程序轉換的巨大區(qū)別所在。
P.4:理想情況下,程序應該是靜態(tài)類型安全的
Reason
理想情況下,程序應該是完全靜態(tài)(編譯時)類型安全的。不幸的是,這是不可能的。問題:
- unions
- casts(數(shù)據(jù)類型轉換)
- array decay(陣列衰變)
- range errors
- narrowing conversions(縮小轉換率)
Note
這些區(qū)域是嚴重問題的根源(例如,崩潰和安全違規(guī))。 我們嘗試提供替代技術。
Enforcement
我們可以根據(jù)需要單獨禁止,限制或檢測各個問題類別,并為各個程序提供可行性。 總是提出另一種選擇。 例如:
- unions -- use
variant(in C++17) - casts -- 盡量減少使用; 模板可以幫助
- array decay -- use
span(from the GSL) - range errors -- use
span
+narrowing conversions -- 盡量減少使用,并在必要時使用narrow或narrow_cast(來自GSL)
P.5:與運行時檢查相比,更喜歡編譯時檢查
Reason
代碼清晰度和高效。 您不需要為編譯時捕獲的錯誤編寫錯誤處理程序。
Example
// Int is an alias used for integers
int bits = 0; // don't: avoidable code
for (Int i = 1; i; i <<= 1)
++bits;
if (bits < 32)
cerr << "Int too small\n";
這個例子無法實現(xiàn)它想要實現(xiàn)的目標(因為溢出是未定義的),應該用簡單的替換static_assert:
// Int is an alias used for integers
static_assert(sizeof(Int) >= 4); // do: compile-time check
或者更好的方法是使用類型系統(tǒng)并用Int32_t替換Int。
Example
void read(int* p, int n); // read max n integers into *p
int a[100];
read(a, 1000); // bad, off the end
better
void read(span<int> r); // read into the range of integers r
int a[100];
read(a); // better: let the compiler figure out the number of elements
替代配方:盡量把類型安全判斷放在編譯時候去做
Enforcement
- 查找指針參數(shù)。
- 查找運行時參數(shù)可能違規(guī)范圍。
P.6:在編譯時無法檢查的內容應該在運行時檢查
Reason
在程序中留下難以檢測的錯誤會導致崩潰和糟糕的結果。
Note
理想情況下,我們在編譯時或運行時捕獲所有錯誤(不是程序員邏輯中的錯誤)。 在編譯時捕獲所有錯誤是不可能的,并且通常無法在運行時捕獲所有剩余錯誤。 但是,如果有足夠的資源(分析程序,運行時檢查,機器資源,時間),我們應該努力編寫原則上可以檢查的程序。
Example, bad
// separately compiled, possibly dynamically loaded
extern void f(int* p);
void g(int n)
{
// bad: the number of elements is not passed to f()
f(new int[n]);
}
在這里,一些關鍵的信息(元素的數(shù)量)被完全"obscured"了,以至于靜態(tài)分析可能變得不可行,當f()是ABI的一部分時,動態(tài)檢查可能非常困難,因此我們無法"instrument" 指針。我們可以將有用的信息嵌入到免費存儲中,但這需要對系統(tǒng)和編譯器進行全局更改。這里的設計使得錯誤檢測非常困難。
Example, bad
當然,我們可以通過指針傳遞元素的數(shù)量:
// separately compiled, possibly dynamically loaded
extern void f2(int* p, int n);
void g2(int n)
{
f2(new int[n], m); // bad: a wrong number of elements can be passed to f()
}
將元素的數(shù)量作為參數(shù)傳遞比僅傳遞指針并依賴于某些(未說明的)約定來更新(并且更常見),以便知道或發(fā)現(xiàn)元素的數(shù)量。 但是(如圖所示),簡單的拼寫錯誤可能會引入嚴重的錯誤。 f2()的兩個參數(shù)之間的連接是常規(guī)的,而不是顯式的。
另外,f2()應該delete它的參數(shù)(或者調用者犯了第二個錯誤?)
Example, bad
標準庫資源管理指針在指向對象時無法傳遞大?。?/p>
// separately compiled, possibly dynamically loaded
// NB: this assumes the calling code is ABI-compatible, using a
// compatible C++ compiler and the same stdlib implementation
extern void f3(unique_ptr<int[]>, int n);
void g3(int n)
{
f3(make_unique<int[]>(n), m); // bad: pass ownership and size separately
}
Example
我們需要將指針和元素數(shù)作為一個整體對象傳遞:
extern void f4(vector<int>&); // separately compiled, possibly dynamically loaded
extern void f4(span<int>); // separately compiled, possibly dynamically loaded
// NB: this assumes the calling code is ABI-compatible, using a
// compatible C++ compiler and the same stdlib implementation
void g3(int n)
{
vector<int> v(n);
f4(v); // pass a reference, retain ownership
f4(span<int>{v}); // pass a view, retain ownership
}
這種設計將元素的數(shù)量作為對象的組成部分,假如錯誤是不可能的,在外部因素可以承擔情況下,動態(tài)(運行時)總是進行動態(tài)檢查。
Example
我們如何轉移所有權和驗證使用所需的所有信息?
vector<int> f5(int n) // OK: move
{
vector<int> v(n);
// ... initialize v ...
return v;
}
unique_ptr<int[]> f6(int n) // bad: loses n
{
auto p = make_unique<int[]>(n);
// ... initialize *p ...
return p;
}
owner<int*> f7(int n) // bad: loses n and we might forget to delete
{
owner<int*> p = new int[n];
// ... initialize *p ...
return p;
}
Example
- ???
- 展示如何通過傳遞多態(tài)基類的接口來避免可能的檢查,當他們真正知道他們需要什么時? 或字符串作為 "free-style"選項
Enforcement
- 標志(指針,計數(shù))式接口(這將標記許多由于兼容性原因無法修復的示例)
- ???
P.7: 盡早發(fā)現(xiàn)運行時錯誤
Reason
避免"mysterious"的崩潰。 避免導致(可能是無法識別的)錯誤結果。
Example
void increment1(int* p, int n) // bad: error-prone
{
for (int i = 0; i < n; ++i) ++p[i];
}
void use1(int m)
{
const int n = 10;
int a[n] = {};
// ...
increment1(a, m); // maybe typo, maybe m <= n is supposed
// but assume that m == 20
// ...
}
這里我們在use1中犯了一個小錯誤,導致數(shù)據(jù)損壞或崩潰。 (指針,計數(shù))式接口調用increment1(),沒有現(xiàn)實的方法來防御超出范圍的錯誤。 如果我們可以檢查超出范圍訪問的下標,那么在訪問p [10]之前不會發(fā)現(xiàn)錯誤。 我們可以提前檢查并改進代碼:
void increment2(span<int> p)
{
for (int& x : p) ++x;
}
void use2(int m)
{
const int n = 10;
int a[n] = {};
// ...
increment2({a, m}); // maybe typo, maybe m <= n is supposed
// ...
}
現(xiàn)在,可以在定義的時候(早期)而不是稍后檢查m <= n。 如果我們只有一個拼寫錯誤,以便我們打算使用n作為界限,那么代碼可以進一步簡化(消除錯誤的可能性):
void use3(int m)
{
const int n = 10;
int a[n] = {};
// ...
increment2(a); // the number of elements of a need not be repeated
// ...
}
Example, bad
不要反復檢查相同的值。 不要將結構化數(shù)據(jù)作為字符串傳遞:
Date read_date(istream& is); // read date from istream
Date extract_date(const string& s); // extract date from string
void user1(const string& date) // manipulate date
{
auto d = extract_date(date);
// ...
}
void user2()
{
Date d = read_date(cin);
// ...
user1(d.to_string());
// ...
}
日期驗證兩次(由Date構造函數(shù))并作為字符串(非結構化數(shù)據(jù))傳遞。
Example
過度檢查可能代價高昂。 有些情況下,早期檢查是愚蠢的,因為您可能不需要該值,或者可能只需要比整體更容易檢查的部分值。 同樣,不要添加更改接口漸近性的有效性檢查(例如,不要向平均復雜度為O(1)的接口添加O(n)檢查。
class Jet { // Physics says: e * e < x * x + y * y + z * z
float x;
float y;
float z;
float e;
public:
Jet(float x, float y, float z, float e)
:x(x), y(y), z(z), e(e)
{
// Should I check here that the values are physically meaningful?
}
float m() const
{
// Should I handle the degenerate case here?
return sqrt(x * x + y * y + z * z - e * e);
}
???
};
射頻的物理定律(e * e <x * x + y * y + z * z)不是不變量,因為可能存在測量誤差。
Enforcement
- 查看指針和數(shù)組:盡早進行范圍檢查,而不是重復檢查
- 查看轉化次數(shù):消除或標記縮小的轉化次數(shù)
- 查找來自輸入的未經(jīng)檢查的值
- 查找結構化數(shù)據(jù)(具有不變量的類的對象)轉換為字符串
- ???
P.8:不要泄漏任何資源
Reason
隨著時間的推移,即使資源的緩慢增長也會耗盡這些資源的可用性。 這對于長期運行的程序尤為重要,但卻是負責任的編程行為的重要組成部分。
Example, bad
void f(char* name)
{
FILE* input = fopen(name, "r");
// ...
if (something) return; // bad: if something == true, a file handle is leaked
// ...
fclose(input);
}
Prefer RAII:
void f(char* name)
{
ifstream input {name};
// ...
if (something) return; // OK: no leak
// ...
}
See also: The resource management section
Note
泄漏通俗地稱為“任何未清理的東西”。 更重要的分類是“任何無法再清理的東西”。 例如,在堆上分配一個對象,然后失去指向該分配的最后一個指針。 此規(guī)則不應該被理解為要求長期對象中分配必須計劃停機期間將返回。 例如,依賴系統(tǒng)保證的清理(例如文件關閉和進程關閉時的內存釋放)可以簡化代碼。 但是,依賴隱式清理的抽象可以簡單,通常更安全。
Note
實施終身安全配置可消除泄漏。 當與RAII提供的資源安全相結合時,它消除了“垃圾收集”的需要(通過不產(chǎn)生垃圾)。 將此與類型和邊界配置文件的強制執(zhí)行相結合,您可以獲得完整的類型和資源安全性,并由工具保證。
Enforcement
- 查看指針:將它們分類為非所有者(默認)和所有者。 在可行的情況下,使用標準庫資源句柄替換所有者(如上例所示)。 或者,使用GSL的
owner標記所有者。 - 尋找裸
new和delete - 查找返回原始指針的已知資源分配函數(shù)(例如
fopen,malloc和strdup)
P.9:不要浪費時間或空間
Reason
This is C++.
Note
您沒有浪費時間和空間來實現(xiàn)目標(例如,開發(fā)速度,資源安全性或測試的簡化)。 “追求效率的另一個好處是,這個過程迫使你更深入地理解這個問題?!?-- Alex Stepanov
Example, bad
struct X {
char ch;
int i;
string s;
char ch2;
X& operator=(const X& a);
X(const X&);
};
X waste(const char* p)
{
if (!p) throw Nullptr_error{};
int n = strlen(p);
auto buf = new char[n];
if (!buf) throw Allocation_error{};
for (int i = 0; i < n; ++i) buf[i] = p[i];
// ... manipulate buffer ...
X x;
x.ch = 'a';
x.s = string(n); // give x.s space for *p
for (gsl::index i = 0; i < x.s.size(); ++i) x.s[i] = buf[i]; // copy buf into x.s
delete[] buf;
return x;
}
void driver()
{
X x = waste("Typical argument");
// ...
}
是的,這是一種諷刺,但我們已經(jīng)看到在生產(chǎn)代碼,更糟的是每一個人的錯誤。 請注意,X的布局保證浪費至少6個字節(jié)(并且很可能更多)。 復制操作的虛假定義會禁用移動語義,因此返回操作很慢(請注意,此處不保證返回值優(yōu)化,RVO)。 對buf使用new和delete是多余的; 如果我們真的需要一個本地字符串,我們應該使用本地字符串。 其他還有一些性能缺陷和無償?shù)牟l(fā)癥。
Example, bad
void lower(zstring s)
{
for (int i = 0; i < strlen(s); ++i) s[i] = tolower(s[i]);
}
是的,這是從實際項目中代碼的例子。 我們留給讀者來弄清楚浪費了什么。
Note
浪費(時間或者空間)的個別例子很少有重要意義,如果是重要的,通常很容易被專家消除。 然而,在代碼庫中廣泛傳播的浪費很容易變得非常重要,專家并不總是像我們希望的那樣可用。 此規(guī)則的目標(以及支持它的更具體的規(guī)則)是在C ++發(fā)生之前消除與使用C ++相關的大多數(shù)浪費。 之后,我們可以查看與算法和要求相關的浪費,但這超出了這些指南的范圍。
Enforcement
許多更具體的規(guī)則旨在實現(xiàn)簡化和消除無償浪費的總體目標。
P.10: 更喜歡不可變數(shù)據(jù)而不是可變數(shù)據(jù)
Reason
關于常數(shù)而不是關于變量的推理更容易。 不可變的東西不能意外地改變。 有時,不變性可以實現(xiàn)更好的優(yōu)化。 您不能在常量上進行數(shù)據(jù)競爭。
See Con: Constants and immutability
P.11:封裝凌亂的結構,而不是傳播代碼
Reason
凌亂的代碼更容易隱藏錯誤,更難寫。 良好的界面使用起來更簡單,更安全。 凌亂的低級代碼會產(chǎn)生更多此類代碼。
Example
int sz = 100;
int* p = (int*) malloc(sizeof(int) * sz);
int count = 0;
// ...
for (;;) {
// ... read an int into x, exit loop if end of file is reached ...
// ... check that x is valid ...
if (count == sz)
p = (int*) realloc(p, sizeof(int) * sz * 2);
p[count++] = x;
// ...
}
這是低級的,冗長的,容易出錯的。 例如,我們“忘記”測試內存耗盡。 相反,我們可以使用vector:
vector<int> v;
v.reserve(100);
// ...
for (int x; cin >> x; ) {
// ... check that x is valid ...
v.push_back(x);
}
Note
標準庫和GSL就是這種理念的例子。 例如,我們使用所設計的庫,而不是搞亂實現(xiàn)關鍵抽象(例如vector,span,lock_guard和future)所需的數(shù)組,聯(lián)合,強制轉換,棘手的生命周期問題,gsl :: owner等。 由比我們通常擁有的更多時間和專業(yè)知識的人實施。 同樣,我們可以而且應該設計和實現(xiàn)更專業(yè)的庫,而不是讓用戶(通常是我們自己)面臨重復獲得低級代碼的挑戰(zhàn)。 這是超集原則子集的變體,是這些準則的基礎。
Enforcement
- 尋找“雜亂的代碼”,例如復雜的指針操作和在抽象實現(xiàn)之外的轉換。
P.12:適當使用支持工具
Reason
“通過機器”可以做得更好。 計算機不會偷懶或厭倦重復性任務。 我們通常有比做反復做常規(guī)任務更好的事情。
Example
運行靜態(tài)分析器以驗證您的代碼是否遵循您希望它遵循的準則。
Note Seen
- Static analysis tools(靜態(tài)分析)
- Concurrency tools(并行分析)
-
Testing tools(測試分析)
還有許多其他類型的工具,例如源代碼存儲庫,構建工具等,但這些工具超出了這些指南的范圍。
Note
注意不要依賴于過于復雜或過度專業(yè)化的工具鏈。 這些可以使您的便攜式代碼不可移植。
P.13:根據(jù)需要使用支持庫
Reason
使用設計良好,文檔齊全且支持良好的庫可節(jié)省時間和精力; 如果您的大部分時間都花在實施上,那么它的質量和文檔可能會比您可以做的更大。 圖書館的成本(時間,精力,金錢等)可以在許多用戶之間共享。 與單個應用程序相比,廣泛使用的庫更有可能保持最新并移植到新系統(tǒng)。 了解廣泛使用的庫可以節(jié)省其他/未來項目的時間。 因此,如果您的應用程序域存在合適的庫,請使用它。
Example
std::sort(begin(v), end(v), std::greater<>());
除非您是排序算法的專家并且有足夠的時間,否則這更可能是正確的,并且比您為特定應用程序編寫的任何內容運行得更快。 您需要一個不使用標準庫(或您的應用程序使用的任何基礎庫)而不是使用它的理由的理由。
Note 默認使用:
Note
如果沒有為重要域存在設計良好,文檔齊全且支持良好的庫,那么您可能應該設計并實現(xiàn)它,然后使用它。