前言
C++ 是永遠(yuǎn)也學(xué)不完的語言,最近發(fā)現(xiàn)了一個(gè)不錯(cuò)的教程 C++ Core Guidelines,希望記錄下自己閱讀時(shí)的心得。
本文主要是為了記錄自己的知識盲區(qū),可能不適用于其他讀者。
一致地定義 copy, move and destroy
如果你需要自定義 copy/move constructor, copy/move assignment operator, destructor 這 5 個(gè)函數(shù),說明你期望做一些默認(rèn)行為之外的事,因此需要保持以下的一致性:
定義了拷貝構(gòu)造函數(shù),就要定義拷貝賦值函數(shù)
-
定義了移動構(gòu)造函數(shù),就要定義移動賦值函數(shù)
X x1; X x2 = x1; // ok x2 = x1; // pitfall: either fails to compile, or does something suspicious 如果一個(gè)類的基類或者成員變量中有任何一個(gè)定義了移動構(gòu)造函數(shù),則該類也應(yīng)該定義移動構(gòu)造函數(shù)。
自定義了析構(gòu)函數(shù),同時(shí)也要定義或者禁用 copy/move
class AbstractBase {
public:
virtual ~AbstractBase() = default;
AbstractBase(const AbstractBase&) = default;
AbstractBase& operator=(const AbstractBase&) = default;
AbstractBase(AbstractBase&&) = default;
AbstractBase& operator=(AbstractBase&&) = default;
};
合理使用 noexcept
將一個(gè)函數(shù)定義為 noexcept 有助于編譯器生成更有效率的 code。因?yàn)椴恍枰涗?exception handler。但是 noexcept 并非隨意使用的。
主要用于不會拋出異常的函數(shù),例如純 C 寫成的函數(shù),也可以是一些非常簡單的例如 setter 和 getter 函數(shù)。對于可能拋出內(nèi)存不足等無法解決的異常的函數(shù),也可以使用。
vector<string> collect(istream& is) noexcept
{
vector<string> res;
for (string s; is >> s;)
res.push_back(s);
return res;
}
非常適合于使用 low-level 接口寫的頻繁使用的函數(shù)。
值得注意的是,以下幾種函數(shù)不允許拋出異常:
- 析構(gòu)函數(shù)
-
swap函數(shù) - move 操作
- 默認(rèn)構(gòu)造函數(shù)
僅在需要操作對象生命周期時(shí)使用智能指針作參數(shù)
(見過不少炫技代碼強(qiáng)行使用智能指針)
最佳方式還是使用引用 T& 作為函數(shù)參數(shù)。使用 T* 不明確所有權(quán),并且需要檢查空指針。使用unique_ptr<T>限制了調(diào)用者也必須使用智能指針。使用shared_ptr<T>會導(dǎo)致性能上的損失(引用計(jì)數(shù)的原子操作)。
// accepts any int*
void f(int*);
// can only accept ints for which you want to transfer ownership
void g(unique_ptr<int>);
// can only accept ints for which you are willing to share ownership
void g(shared_ptr<int>);
// doesn't change ownership, but requires a particular ownership of the caller
void h(const unique_ptr<int>&);
// accepts any int
void h(int&);
使用 T&& 和 std::forward 轉(zhuǎn)發(fā)參數(shù)
如果參數(shù)被傳遞進(jìn)某個(gè)函數(shù)中,但是并不直接在這個(gè)函數(shù)中使用,則使用T&& 傳遞,并只進(jìn)行std::forward 操作來實(shí)現(xiàn)“完美轉(zhuǎn)發(fā)”。參數(shù)是不是 const, 是左值還是右值都會被保留,完美傳遞給下一層函數(shù)。
template <class F, class... Args>
inline auto invoke(F f, Args&&... args) {
return f(forward<Args>(args)...);
}
返回 T& 當(dāng)你不期望拷貝或者返回空
例如:
class Car
{
array<wheel, 4> w;
// ...
public:
wheel& get_wheel(int i) { Expects(i < w.size()); return w[i]; }
// ...
};
void use()
{
Car c;
wheel& w0 = c.get_wheel(0); // w0 has the same lifetime as c
}
錯(cuò)誤的例子:
int& f()
{
int x = 7;
// ...
return x; // Bad: returns reference to object that is about to be destroyed
}
lambda
[captures] (params) -> ret {body}
捕獲變量:
-
=表示拷貝 (而非 const 引用),強(qiáng)調(diào) correctness。是按值傳入的,但是變量在 lambda 內(nèi)是 const 的。值得注意的是,=捕獲的變量在 lambda 構(gòu)造的時(shí)候就確定了,而非調(diào)用的時(shí)候確定。 -
&表示引用,強(qiáng)調(diào) efficiency。并沒有 const 引用的捕獲方式。
假設(shè) message 是一個(gè)較大的網(wǎng)絡(luò)消息,拷貝比較昂貴??梢圆东@單個(gè)變量:
std::for_each(begin(sockets), end(sockets), [&message](auto& socket)
{
socket.send(message);
});
值得注意的是,使用 [=] 在類內(nèi)捕獲變量時(shí),會捕獲 this,導(dǎo)致出現(xiàn)不期望的結(jié)果(修改了某個(gè)類成員變量會影響按值傳遞的 lambda 的行為)。在 c++20 標(biāo)準(zhǔn)中,[=]已經(jīng)不再捕獲 this。
class My_class {
int x = 0;
// ...
void f() {
int i = 0;
// ...
auto lambda = [=]{ use(i, x); }; // BAD: "looks like" copy/value capture
// [&] has identical semantics and copies the this pointer under the current rules
// [=,this] and [&,this] are not much better, and confusing
x = 42;
lambda(); // calls use(0, 42);
x = 43;
lambda(); // calls use(0, 43);
// ...
auto lambda2 = [i, this]{ use(i, x); }; // ok, most explicit and least confusing
// ...
}
};
什么時(shí)候使用struct,什么時(shí)候使用class
使用 class 說明存在 invariant (即存在一些邏輯約束,不能任意更改其值,如果所有的類成員獨(dú)立,則不存在 invariant)。使用 struct 說明允許獨(dú)立更改每一個(gè)數(shù)據(jù)成員。例如:
struct Pair {
string name;
int volume;
};
class Date {
public:
// validate that {yy, mm, dd} is a valid date and initialize
Date(int yy, Month mm, char dd);
// ...
private:
int y;
Month m;
char d; // day
}
簡單來講,如果你定義了任意一個(gè)類成員為 private,則應(yīng)該用 class。
按照成員變量的順序初始化
構(gòu)造時(shí)的初始化順序是按照成員變量的順序來的。如果不按照該順序,則會導(dǎo)致非預(yù)期的行為。
#include <iostream>
class A {
private:
int num1;
int num2;
public:
explicit A(int n): num2(n), num1(++n) {
// expect 11, 10 but get 11, 11
std::cout << num1 << ", "<< num2 << std::endl;
}
};
int main(int argc, char const *argv[]) {
A a(10);
return 0;
}
基類應(yīng)該禁止拷貝,但是提供一個(gè) clone() 虛函數(shù)
這是為了防止對象被“截?cái)唷?。因?yàn)橐粋€(gè)普通的拷貝操作只會拷貝基類成員。對于一個(gè)有虛函數(shù)的類(會被繼承),不應(yīng)該有拷貝構(gòu)造函數(shù)和拷貝賦值函數(shù)。
class B { // GOOD: base class suppresses copying
public:
B(const B&) = delete;
B& operator=(const B&) = delete;
virtual unique_ptr<B> clone() { return /* B object */; }
// ...
};
class D : public B {
string more_data; // add a data member
unique_ptr<B> clone() override { return /* D object */; }
// ...
};
auto d = make_unique<D>();
auto b = d.clone(); // ok, deep clone
這里需要注意的是,無論在基類還是派生類中,clone() 返回的都是基類的智能指針 unique_ptr<B>。
使用工廠模式來定制初始化時(shí)的 "virtual behavior"
不要在構(gòu)造函數(shù)中調(diào)用虛函數(shù)。
#include <iostream>
class Base {
public:
Base() noexcept {
init();
}
virtual ~Base() {
std::cout << "base deleted" << std::endl;
}
virtual void init() {
std::cout << "init base" << std::endl;
}
};
class Derived: public Base {
public:
~Derived() {
std::cout << "derived deleted" << std::endl;
}
virtual void init() override {
std::cout << "init derived" <<std::endl;
}
};
int main(int argc, char const *argv[]) {
Base* a = new Derived();
a->init();
delete a;
return 0;
}
以上程序的意圖是想通過派生類不同的 init() 實(shí)現(xiàn)來進(jìn)行不同的初始化。然而這并不能如預(yù)期實(shí)現(xiàn)。輸出結(jié)果是:
init base
init derived
derived deleted
base deleted
顯然,構(gòu)造的時(shí)候用的仍然是基類的 init()。
從概念上講,因?yàn)樵跇?gòu)造派生類前會先構(gòu)造基類,此時(shí)派生類的實(shí)例還沒構(gòu)造完成,從cpp語言層面來講不允許去操作初始化的成員。
從實(shí)現(xiàn)上講,在構(gòu)造實(shí)例時(shí)我們會設(shè)置虛指針 vptr,該指針會隨著類繼承的順序改變指向,如果有個(gè)更晚的派生類被構(gòu)造,則會指向該類的虛表vtable,如此直到實(shí)例構(gòu)造結(jié)束,所以 vptr 的指向是由最后調(diào)用的構(gòu)造函數(shù)確定的。因此,在構(gòu)造到基類時(shí),只會指向基類的虛表。
為了解決這個(gè)問題,我們一般使用工廠函數(shù)。
工廠函數(shù)一般返回 unique_ptr。
#include <iostream>
#include <memory>
class Base {
protected:
Base() {} // avoid directly invoking
public:
virtual void init() {std::cout << "base init" << std::endl;}
virtual ~Base() {std::cout << "base deleted" << std::endl;}
template<typename T, typename... Args>
static std::unique_ptr<T> Create(Args &&... args) {
auto p = std::make_unique<T>(std::forward<Args>(args)...);
p->init();
return p;
}
};
class Derived : public Base {
public:
~Derived() {std::cout << "derived deleted" << std::endl;}
virtual void init() override {std::cout << "derived init" << std::endl;}
};
int main(int argc, char const *argv[]) {
auto p = Base::Create<Derived>();
p->init();
return 0;
}
注意這里將基類的構(gòu)造函數(shù)設(shè)置為 protected 避免被誤用于構(gòu)造。
通過 Create() 方法可以方便地構(gòu)造實(shí)例。輸出結(jié)果為:
derived init
derived init
derived deleted
base deleted
委托構(gòu)造函數(shù) (delegating constructors)
委托構(gòu)造函數(shù)是 c11 引入的新特性,可以在一個(gè)構(gòu)造函數(shù)中調(diào)用另一個(gè)構(gòu)造函數(shù)。這樣我們就能夠避免維護(hù)重復(fù)的構(gòu)造函數(shù)代碼。
例如,如果不使用該特性,我們需要在每個(gè)構(gòu)造函數(shù)中檢查參數(shù)。
class Date { // BAD: repetitive
int d;
Month m;
int y;
public:
Date(int ii, Month mm, year yy)
:i{ii}, m{mm}, y{yy}
{ if (!valid(i, m, y)) throw Bad_date{}; }
Date(int ii, Month mm)
:i{ii}, m{mm} y{current_year()}
{ if (!valid(i, m, y)) throw Bad_date{}; }
// ...
};
其語法如下,注意使用大括號。
class Date2 {
int d;
Month m;
int y;
public:
Date2(int ii, Month mm, year yy)
:i{ii}, m{mm}, y{yy}
{ if (!valid(i, m, y)) throw Bad_date{}; }
Date2(int ii, Month mm)
:Date2{ii, mm, current_year()} {}
// ...
};
繼承構(gòu)造函數(shù) (inheriting constructors)
有時(shí)候我們需要為一個(gè)類擴(kuò)展一些方法,但是不改變其構(gòu)造。這時(shí)候我們?nèi)绻褂美^承,則需要對基類的每個(gè)構(gòu)造函數(shù)都重復(fù)以下代碼:
#include <iostream>
class Base {
public:
explicit Base(int a): a_(a) {}
protected:
int a_;
};
class Derived: public Base {
public:
explicit Derived(int a): Base(a) {} // repeat this for all constructors
// methods
void describe() {
std::cout << a_ << std::endl;
}
};
如果使用 using 關(guān)鍵字實(shí)現(xiàn)繼承構(gòu)造函數(shù),則會簡單的多:
#include <iostream>
class Base {
public:
explicit Base(int a): a_(a) {}
protected:
int a_;
};
class Derived: public Base {
public:
using Base::Base; // inherit from base class
// methods
void describe() {
std::cout << a_ << std::endl;
}
};
int main(int argc, char const *argv[]) {
Derived d(10);
d.describe();
return 0;
}
copy assignment 和 move assignment
copy assignment 和 move assignment 在處理 self assignment 的時(shí)候會有區(qū)別。因?yàn)閷⒆约?move 到自己可能導(dǎo)致內(nèi)存錯(cuò)誤。在 copy assignment 中我們?yōu)榱诵士梢灾苯?copy 不做這個(gè)check, 而在 move assignment 中必須做。
class Foo {
string s;
int i;
public:
Foo& operator=(const Foo& a);
Foo& operator=(Foo&& a);
// ...
};
Foo& Foo::operator=(const Foo& a)
{
s = a.s;
i = a.i;
return *this;
}
Foo& Foo::operator=(Foo&& a) noexcept // OK, but there is a cost
{
if (this == &a) return *this;
s = std::move(a.s);
i = a.i;
return *this;
}
不要為虛函數(shù)提供默認(rèn)參數(shù)
override 的時(shí)候并不會覆蓋原來的默認(rèn)參數(shù)。這是比較好理解的,虛表中保存的是函數(shù)指針,跟參數(shù)無關(guān)。
#include <iostream>
class Base {
public:
virtual int multiply(int val, int factor=2) = 0;
virtual ~Base() {}
};
class Derived : public Base {
public:
int multiply(int val, int factor=10) final {
return val * factor;
}
};
int main(int argc, char const *argv[]) {
Derived d;
Base& b = d;
std::cout << b.multiply(10) << std::endl; // 20
std::cout << d.multiply(10) << std::endl; // 100
return 0;
}
使用指針或者引用訪問多態(tài)對象
否則會導(dǎo)致得到的是“截?cái)唷敝粱惖膶ο蟆?/p>
// Access polymorphic objects through pointers and references
#include <iostream>
struct B {
int a=0;
virtual void func() {
std::cout << "a = " << a << std::endl;
}
};
struct D : public B {
int b=1;
void func() final {
std::cout << "b = " << b << std::endl;
}
};
void fault_use(B b) {
b.func();
}
void correct_use(B& b) {
b.func();
}
int main(int argc, char const *argv[]) {
D d;
fault_use(d); // a = 0
correct_use(d); // b = 1
return 0;
}
使用 enum class 替換 enum 和宏
enum 存在三個(gè)主要缺點(diǎn):
- 與整形之間的隱式轉(zhuǎn)換
可以比較兩個(gè)不同枚舉類型的大小。 - 作用域
在一個(gè)enum使用過的變量名不能用于另一個(gè)enum。 - 不同編譯器實(shí)現(xiàn)
enum的底層數(shù)據(jù)類型不同
使用 signed 還是 unsigned,使用 8bit,16bit 還是 32bit,不同編譯器實(shí)現(xiàn)不一樣。
宏的缺點(diǎn):
- 不遵從 scope 和類型的規(guī)則
- 宏在編譯后變量名就消失了,不利于 debug
此外,enum class 中的變量命名需要避免全部大寫,與宏定義混淆。
#include <iostream>
enum class Color{
red = 0xFF0000,
green = 0x00FF00,
blue = 0x0000FF
};
void print_color(Color c) {
switch (c) {
case Color::red:
std::cout << "red" << std::endl;
break;
case Color::green:
std::cout << "green" << std::endl;
break;
case Color::blue:
std::cout << "blue" << std::endl;
break;
default:
std::cout << "unknown" << std::endl;
}
}
int main(int argc, char const *argv[]) {
Color c = Color::blue;
print_color(c);
return 0;
}
使用 weak_ptr 避免循環(huán)引用
循環(huán)引用會導(dǎo)致無法釋放內(nèi)存。例如下面的代碼,如果 Man 和 Woman 都用 shared_ptr 相互引用,則會導(dǎo)致雙方無法銷毀。
由于不具備所有權(quán),weak_ptr是不能直接使用的引用對象的,必須通過lock()方法生成一個(gè) shared_ptr,暫時(shí)獲取所有權(quán)再使用。
// use weak_ptr to break cycles of shared_ptr
#include <iostream>
#include <memory>
class Woman;
class Man {
public:
void set_wife(const std::shared_ptr<Woman> &w) { wife_ = w; }
void walk_the_dog() { std::cout << "man walks the dog" << std::endl; }
~Man() { std::cout << "Man destroyed" << std::endl; }
private:
std::shared_ptr<Woman> wife_;
};
class Woman {
public:
void set_husband(const std::weak_ptr<Man> &m) { husband_ = m; }
void use_husband() {
if (auto husband = husband_.lock()) {
husband->walk_the_dog();
}
}
~Woman() { std::cout << "Woman destroyed" << std::endl; }
private:
std::weak_ptr<Man> husband_;
};
int main(int argc, char const *argv[]) {
auto m = std::make_shared<Man>();
auto w = std::make_shared<Woman>();
m->set_wife(w);
w->set_husband(m);
w->use_husband();
return 0;
}
不要使用 C 風(fēng)格的變長參數(shù)函數(shù)(variadic function)
不是類型安全的,而且需要復(fù)雜的語法和轉(zhuǎn)換。推薦使用cpp模板和重載實(shí)現(xiàn)。
#include <iostream>
void print_error(int severity) {
std::cerr << '\n';
std::exit(severity);
}
template<typename T, typename... Ts>
constexpr void print_error(int severity, T head, Ts &&... tail) {
std::cerr << head << " ";
print_error(severity, std::forward<Ts>(tail)...);
}
int main(int argc, char const *argv[]) {
print_error(1); // Ok
print_error(2, "hello", "world"); // Ok
std::string s = "my";
print_error(3, "hello", s, "world"); // Ok
print_error(4, "hello", nullptr); // compile error
return 0;
}
使用 std::call_once 或者靜態(tài)局部變量實(shí)現(xiàn)“初始化一次”
從 c11 開始,靜態(tài)局部變量的實(shí)現(xiàn)就是線程安全的了。你不需要自己實(shí)現(xiàn) double-checked locking 來初始化(通常你的實(shí)現(xiàn)都是錯(cuò)誤的)。
// Do not write your own double-checked locking for initialization
#include <iostream>
class LargeObj {
public:
LargeObj() { std::cout << "Large object initialized..." << std::endl; }
};
void func() {
static LargeObj obj;
static std::once_flag flg;
std::call_once(flg, []() { std::cout << "call once..." << std::endl; });
std::cout << "invoke func..." << std::endl;
}
int main(int argc, char const *argv[]) {
func();
func();
func();
return 0;
}
以上代碼輸出為:
Large object initialized...
call once
invoke func...
invoke func...
invoke func...
如果你頭鐵非要使用 double-checked locking,以下是可以保證線程安全的最佳實(shí)現(xiàn)。
mutex action_mutex;
atomic<bool> action_needed;
if (action_needed.load(memory_order_acquire)) {
lock_guard<std::mutex> lock(action_mutex);
if (action_needed.load(memory_order_relaxed)) {
take_action();
action_needed.store(false, memory_order_release);
}
}
使用 using 替代 typedef
可讀性。例如,同樣定義一個(gè)函數(shù)指針類型:
typedef int (*PFI)(int); // OK, but convoluted
using PFI2 = int (*)(int); // OK, preferred
此外,using 還能用于模板類型:
template<typename T>
typedef int (*PFT)(T); // error
template<typename T>
using PFT2 = int (*)(T); // OK