C++ Core Guidelines 讀書筆記

前言

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)存。例如下面的代碼,如果 ManWoman 都用 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
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容