現代C++(Modern C++)基本用法實踐:N、其他零散的常用特性

2023-07-18 12:01:02

概述

這一篇簡單介紹一些其他的比較實用的特性,如果讀者想了解現代C++的全部特性,參考:cpp reference

其他特性

預置和棄置函數default&delete

在 C++11 中引入了 defaultdelete 關鍵字,允許程式設計師更加明確地控制類的預設操作(如預設建構函式,拷貝建構函式,拷貝賦值運運算元,解構函式等)。

default

default 關鍵字用於明確地要求編譯器生成預設的實現。例如,如果你想要一個類擁有預設的建構函式,你可以這樣做:

class Obj {
public:
    Obj() = default;  // 使用編譯器生成的預設建構函式
};

delete

delete 關鍵字用於禁止編譯器生成預設的實現。例如,如果你不希望你的類被拷貝,你可以這樣做:

class Obj {
public:
    Obj(const Obj&) = delete;  // 禁止拷貝建構函式
    Obj& operator=(const Obj&) = delete;  // 禁止拷貝賦值運運算元
};

在這個例子中,如果你嘗試拷貝 Obj 的範例,編譯器將會報錯。

這兩個關鍵字可以讓你更明確地控制類的行為,防止編譯器生成你不希望的預設操作。

繼承和多型的控制final&override

override

override 關鍵字用於明確表示一個虛擬函式覆蓋了它的基礎類別中的版本。這可以幫助編譯器檢查你的程式碼,防止你因為拼寫錯誤或引數不匹配而無意中沒有覆蓋基礎類別的函數。例如:

class Base {
public:
    virtual void foo(int) {}
};

class Derived : public Base {
public:
    void foo(int) override;  // 明確表示這個函數覆蓋了基礎類別的版本
};

final

final 關鍵字可以用於類或虛擬函式。當用於類時,它表示這個類不能被繼承。例如:

class Base final {};  // 這個類不能被繼承

class Derived : public Base {};  // 錯誤:Base 是 final 的

final 用於虛擬函式時,它表示這個函數在派生類中不能被覆蓋。例如:

class Base {
public:
    virtual void foo() final;  // 這個函數不能被覆蓋
};

class Derived : public Base {
public:
    void foo();  // 錯誤:Base::foo 是 final 的
};

這兩個關鍵字可以更明確地控制類的繼承和多型行為,防止錯誤的覆蓋或繼承。

有作用域的列舉

在 C++11 之前,列舉型別的值可以隱式轉換為整數,而且列舉型別的成員在列舉型別的作用域之外是可見的。這可能會導致命名衝突和型別安全問題。

作用域列舉通過使用 enum class 關鍵字來定義,如下所示:

enum class Color {
    Red,
    Green,
    Blue
};

作用域列舉的成員只能通過作用域解析運運算元 :: 來存取,這可以避免命名衝突。例如,不能直接寫 Red,而應該寫 Color::Red

此外,作用域列舉的值不能隱式轉換為整數,這有助於提高型別安全

列表初始化

C++11 引入了一種新的初始化語法,通常被稱為列表初始化或統一初始化。這種語法使用花括號 {} 來初始化物件,可以用於幾乎所有的初始化情況。

如:

int a = {5};  // 初始化基本型別
std::string s = {"hello"};  // 初始化類型別
int arr[] = {1, 2, 3, 4, 5};  // 初始化陣列
std::vector<int> v = {1, 2, 3, 4, 5};  // 初始化容器
struct Point {int x, y;} p = {5, 10};  // 初始化聚合型別

列表初始化有幾個優點:

  1. 統一的語法:列表初始化可以用於所有的初始化情況,包括基本型別、陣列、容器、聚合型別等。

  2. 防止窄化轉換:列表初始化不允許窄化轉換,即從一個型別到另一個型別的轉換可能丟失資訊。如,試圖用浮點數初始化一個整數,或者用一個大的整數初始化一個小的整數,這樣的程式碼將無法通過編譯。

int a = {3.14};  // 錯誤:窄化轉換
char c = {300};  // 錯誤:窄化轉換
  1. 初始化類的成員:在類別建構函式的初始化列表中,可以使用列表初始化來初始化類的成員。
class MyClass {
public:
    std::vector<int> v;
    MyClass() : v{1, 2, 3, 4, 5} {}  // 使用列表初始化來初始化類的成員
};

列表初始化是一種非常有用的特性,可以幫助你編寫更清晰、更安全的程式碼。

nullptr 空指標

在C++11之前,我們通常使用NULL來表示空指標。然而,NULL其實就是整數0,這可能會導致一些問題。例如,當函數過載中有一個接受int引數的版本和一個接受指標引數的版本時,如果你傳遞NULL給這個函數,編譯器會選擇接受int引數的版本,這可能不是你想要的結果。

為了解決這個問題,C++11引入了nullptr關鍵字來表示空指標。nullptr的型別是nullptr_t,它可以隱式轉換為所有的指標型別,但不能轉換為其他的型別。這使得nullptr在函數過載中的行為更符合預期。

下面是一個例子:

void f(int) {
    std::cout << "f(int) called" << std::endl;
}

void f(char*) {
    std::cout << "f(char*) called" << std::endl;
}

int main() {
    f(NULL);      // 輸出 "f(int) called"
    f(nullptr);   // 輸出 "f(char*) called"
    return 0;
}

在這個例子中,當你傳遞NULLf函數時,編譯器選擇了接受int引數的版本。但是當你傳遞nullptrf函數時,編譯器選擇了接受指標引數的版本。這是因為nullptr的型別是nullptr_t,它可以隱式轉換為char*,但不能轉換為int

因此,C++11推薦使用nullptr來表示空指標,而不是NULL或者0。

noexcept 不會丟擲異常承諾

noexcept是C++11引入的一個新關鍵字,用於指定函數是否會丟擲異常。如果一個函數被宣告為noexcept,那麼它保證不會丟擲任何異常。如果在執行時該函數確實丟擲了異常,那麼程式將呼叫std::terminate來立即結束執行。

如:

void f() noexcept {
    // 這個函數保證不會丟擲任何異常
}

void g() {
    throw std::runtime_error("An error occurred");  // 這個函數可能會丟擲異常
}

使用noexcept關鍵字有兩個主要的好處:

  1. 效能優化:如果編譯器知道一個函數不會丟擲異常,那麼它可能會生成更優化的程式碼。例如,一些需要異常安全保證的操作(如物件的移動)在知道不會丟擲異常的情況下,可以被編譯器優化。

  2. 提供更清晰的介面:通過在函數宣告中使用noexcept關鍵字,你可以明確地告訴呼叫者該函數不會丟擲任何異常。這可以幫助呼叫者更好地理解函數的行為,並據此來編寫程式碼。

需要注意的是,noexcept是一個承諾,如果宣告一個函數為noexcept,那麼你需要確保它在任何情況下都不會丟擲異常。如果不能保證這一點,最好不要使用noexcept關鍵字。

另外,你也可以使用noexcept運運算元來檢查一個表示式是否保證不丟擲異常:

static_assert(noexcept(f()));  // 編譯時檢查f()是否不丟擲異常

三路比較(c++ 20)

假設我們有一個 Person 類,它有一個名字和年齡屬性。我們想要比較兩個 Person 物件,首先比較他們的名字,如果名字相同,再比較他們的年齡。

在C++20中,我們可以使用三路比較運運算元 <=> 來實現這個比較邏輯

#include <string>
#include <compare>

struct Person {
    std::string name;
    int age;

    auto operator<=>(const Person& other) const {
        if (auto cmp = name <=> other.name; cmp != 0) {
            return cmp;
        }
        return age <=> other.age;
    }
};

#include <iostream>

int main() {
    Person alice{"Alice", 20};
    Person bob{"Bob", 20};
    Person charlie{"Charlie", 25};

    std::cout << ((alice <=> bob) < 0 ? "Alice < Bob" : "Alice >= Bob") << std::endl;
    std::cout << ((alice <=> charlie) < 0 ? "Alice < Charlie" : "Alice >= Charlie") << std::endl;
    std::cout << ((bob <=> charlie) < 0 ? "Bob < Charlie" : "Bob >= Charlie") << std::endl;
}

operator<=> 首先比較 name,如果 name 不相同,就返回 name 的比較結果;如果 name 相同,就比較 age,並返回 age 的比較結果。我們也可以使用auto operator<=>(const Person& other) const = default讓編譯器生成,它會按照成員的宣告順序比較每個成員。

值得一提的特性

  • alignof 與 alignas 記憶體對齊相關
  • static_assert 靜態斷言
  • c++17:if和switch語句中進行初始化,如if (int i = f(); i > 10) {}switch (int i = f(); i) {}
  • 聚合初始化(花括號),c++20允許實用圓括號

C++20 一些新的、未實踐但感覺有用的特性

  • 協程
  • 模組
  • 限定與概念
  • 縮略函數模板
  • DR :陣列 new 可推導陣列長度