摘要訊息 : 一些 C++ 17 引入的小特性.

0. 前言

C++ 17 引入的絕大多數重要特性我們基本都介紹完了, 可以在 Jonny'Blog 中搜尋 C++ 17 閱覽這些文章. 還有一些小特性也是 C++ 17 引入的, 本文文章將通過羅列的方式介紹一部分特性.

更新紀錄 :

  • 2022 年 6 月 5 日進行第一次更新和修正.

1. 擴展的 static_assert

C++ 11 引入 static_assert 關鍵字之後, 有不少討論都是為什麼 static_assert 必須要給出錯誤指引字串. 有時候為了方便和對付編碼錯誤, 我們可能會這樣去寫 static_assert(constant_boolean_expression, "");. 有不少人都建議, 後面的字串應該是可選的. C++ 17 提案 N3928《Extending static_assert, v2》提出了不帶有信息的 static_assert, 也就是說, 現在可以這樣寫 : static_assert(constant_boolean_expression);.

2. 移除三字符組

在很早之前, 電腦的鍵盤並不能輸入所有程式設計所必須的字元, 因此 C 引入了三字符組 :

三字符組 替換為
??= #
??/ \
??' ^
??( [
??) ]
??! |
??< {
??> }
??- ~

如今, 這些三字符組已經沒有存在的必要了. 所以, C++ 17 提案 N4086《Removing trigraphs??!》提出了移除這些特性. 雖然這篇標題上寫的是三字符組 ??!, 但是實際上所有的三字符組都被移除了.

3. 樣板參數中的樣板

在 C++ 17 之前, 樣板參數中的樣板只能寫成 template <template <typename> class T>, 因為T 必定是一個樣板類別, 所以不能把 class 改為 typename. C++ 17 提案 N4051《Allow typename in a template template parameter》提出解除這個限制. 因此現在樣板參數中的樣板可以寫成 template <template <typename> typename T>.

4. UTF-8 前綴

C++ 20 引入了 char8_t, 但是在此之前的 C++ 17 提案 N4267《Adding u8 character literals》已經提出了引入了 UTF-8 字元或者字串前綴. 為了表示一個 UTF-8 字元, 我們可以在字元前面增加 u8 : char c {u8'\\'};. 為了表示一個 UTF-8 字串, 我們可以在字串前面增加 u8 : const char str[] {u8"abc"};. 在 C++ 20 之後, 應該把 char 型別更正為 char8_t.

5. 多重巢狀名稱空間

對於多重的巢狀名稱空間, 在 C++ 17 之前必須明確一層一層地寫出 :

namespace A {
    namespace B {
        namespace C {
            namespace D {
                //...
            }
        }
    }
}

但是, 如果除了最後一個名稱空間中有內容, 剩餘的名稱空間中都不存在任何內容, 那麼這樣寫顯得十分繁瑣. C++ 17 提案 N4230《Nested namespace definition (revision 2)》提出了簡化版本的寫法 : namespace A::B::C::D { /* ... */ }. 如果名稱空間 A, BC 之前沒有被宣告, 那麼這樣的寫法相當於宣告了這些名稱空間.

6. 名稱空間和列舉的屬性

屬性這個特性在 C++ 11 被正式引入, 但是它們在 C++ 17 之前暫時無法用於列舉和名稱空間. C++ 17 提案 N4266《Attributes for namespaces and enumerators》提出為名稱空間和列舉增加屬性支援. 對於名稱空間的屬性可以寫在 namespace 和名稱空間的名字之間 : namespace [[attribution]] A { /* ... */ }. 對於列舉也是類似, 當然列舉還還支援屬性寫在型別之後 : enum [[attribution_1]] A : int [[attribution_2]] { /* ... */ }.

7. 非型別樣板引數的轉型

對於非型別樣板引數, 其轉型限制是比較多的. C++ 17 提案 N4268《Allow constant evaluation for all non-type template arguments》提出了解除一些限制, 允許 :

  • 陣列至陣列指標的轉型;
  • 函式至函式指標的轉型;
  • 限定符的轉型;
  • nullptr 向任意指標型別和成員指標型別的轉型.

另外, 針對非型別樣板引數, 在 C++ 17 中只要不是

  • 子物件 (陣列中的元素, 成員變數),
  • 臨時物件,
  • 字面值字串,
  • typeid 表達式的結果,
  • 預先定義的 __func__ 變數,

幾乎都可以支援.

int arr[] {1, 2, 3};
struct S {
    int a;
    static int b;
} s;
template <int *>
class Foo {};
int main(int argc, char *argv[]) {
    Foo<&arr[1]> f1;        // Error
    Foo<arr> f2;        // OK
    Foo<&s.a> f3;       // Error
    Foo<&s.b> f4;       // OK
}

8. 移除 register 關鍵字

早在 C++ 11 時, register 關鍵字已經被遺棄, C++ 17 提案 P0001R1《Remove Deprecated Use of the register Keyword》提出從 C++ 中移除這個關鍵字, 作為保留以供未來使用. register 關鍵字是用於標識一個變數 register int a = 0; 來告訴編碼器這個變數 a 應該放置在暫存器中. 但是現代編碼器幾乎不需要用戶明確標識, 編碼器自身就可以判斷某個變數是否適合存放於某個存儲中. 因此, 這個關鍵字對於 C++ 的影響已經近乎為零.

9. 移除 operator++(bool)

C++ 17 提案 P0002R1《Remove Deprecated operator++(bool)提出, 移除早在 C++ 98 中已經被遺棄的特性 : 針對 bool 型別變數的 ++ 運算 :

auto b {false};
b++;
++b;

上述寫法在 C++ 17 中已經不會被編碼器編碼通過.

10. __has_include

C++ 17 提案 P0061R1__has_include for C++17》為 C++ 帶來了一個巨集 __has_include. 它用於判定某個標頭檔是否已經被包含 :

#if __has_include(<iostream>)
using std::endl;
#else
#include <iostream>
using std::endl;
#endif

11. 改進 Range-For

目前的 Range-For 限制過多, 例如尾後疊代器無法在 Range-For 中進行前進或者後退等的操作以及兩個疊代器的型別必須相同等等. C++ 17 提案 P0061R1 《Generalizing the Range-Based For Loop》提出解除這些限制, 實際上是為了 C++ 20 中的 Ranges 準備的 (在當時還是 Ranges TS). 一個 Range-For 原來會被這樣解釋 :

{
    auto &&range {for-range-init};
    for(auto begin {begin-init}, end {end-init}; begin not_eq end; ++begin) {
        for-range-decl = *begin;
        // ...
    }
}

使用 auto 進行宣告就必定代表著 beginend 的型別相同. 而 P0061R1 提出解除限制後, 一個 Range-For 就可能會被這樣解釋 :

{
    auto &&range {for-range-init};
    auto begin {begin-init};
    auto end {end-init};
    for(; begin not_eq end; ++begin) {
        for-range-decl = *begin;
        // ...
    }
}

12. 為 enum class 增加建構規則

在 C++ 11 引入 enum class 之後, C++ 中就存在一種宣告獨一無二的整型型別的方式 : enum class E : unsigned long {};. 所有型別為 E 的變數都不會向其它整型型別發生轉型. 如果某個時刻, 我們恰好需要這樣的型別以避免程式碼交互過程中的轉型, 那麼使用 enum class 來宣告就非常合適. 但是, 如果需要通過整型型別來宣告 E 的變數, 是需要進行轉型的 : E a1 {static_cast<E>(1)};. 但是, 如果這樣去寫, 就不需要轉型 :

struct Foo {
    unsigned long a;
};
Foo a0 {1};     // OK
Foo a1 = {2};       // OK

借鑑類似的程式碼, C++ 17 提案 P0138R2《Construction Rules for enum class Values》除了提出為 enum class 增加建構規則之後, 還避免了上述兩個情況發生衝突. 在原類別建構規則不變的情況下, 允許非縮小轉換下, 利用其它型別來初始化一個限定作用範圍的列舉型別 :

enum class E {};
E func(E) {
    return {1};     // Error
}
int main(int argc, char *argv[]) {
    E e1 {1};        // OK
    E e2 = 1;        // Error
    E e3 = {1};      // Error
    E e4(1);     // Error
    E e5(E {1});        // OK
    E e6 {E {1}};       // OK
    func({1});      // Error
    func(E {1});        // OK
    E *p {new E {1}};       // OK
}

13. 十六進制浮點數字面值

從 C++ 11 起, 標準樣板程式庫已經支援十六進制的浮點數常數的輸入與輸出, 但是這並不是作為核心語言的部分. C++ 17 提案 P0245R1《Hexadecimal floating literals for C++》提出, 讓 C++ 支援十六進制的浮點數字面值常數 :

#include <iostream>

int main(int argc, char *argv[]) {
    std::cout << 0x10.1p0 << std::endl;     // 輸出結果 : 16.0625
}

其中, p 是一種記數法則, 類似於 e. 上述程式碼中的 0x10.1p0 = 10.1_{\mathrm {hex}} \times 2^{0}.

14. 屬性名稱空間

C++ 17 為名稱空間引入了屬性, C++ 17 提案 P0028R4《Using attribute namespaces without repetition》增加了屬性名稱空間的支援 : namespace [[attribute namespace::attr]] A { /* ... */ }.

15. 非標準屬性支援

編碼器通常會支援比 C++ 標準更多的屬性, 因此 C++ 17 提案 P0283R2《Standard and non-standard attributes》提出, 對於未知的屬性應該作忽略處理 (一般來說, 編碼器可能會擲出一個警告). 這大大提高了 C++ 程式碼在不同編碼器之間的可攜性. 例如,

int [[unknown_attribute]] a;
[[abc]] int [[ccc]] b [[lll]];

在 Clang 下會產生如下的警告 :

main.cpp:5:11: warning: unknown attribute 'unknown_attribution' ignored [-Wunknown-attributes]
int [[unknown_attribution]] a;
^
main.cpp:5:11: warning: unknown attribute 'unknown_attribution' ignored [-Wunknown-attributes]
main.cpp:6:19: warning: unknown attribute 'ccc' ignored [-Wunknown-attributes]
[[abc]] int [[ccc]] b [[lll]];
^
main.cpp:6:7: warning: unknown attribute 'abc' ignored [-Wunknown-attributes]
[[abc]] int [[ccc]] b [[lll]];
^
main.cpp:6:19: warning: unknown attribute 'ccc' ignored [-Wunknown-attributes]
[[abc]] int [[ccc]] b [[lll]];
^
main.cpp:6:29: warning: unknown attribute 'lll' ignored [-Wunknown-attributes]
[[abc]] int [[ccc]] b [[lll]];
^
6 warnings generated.