摘要訊息 : 屬性可以讓編碼器生成效能更佳的程式碼.

0. 前言

C++ 11 之前, 你可能見過 __attribute__, __attribute, __declspec#pragma 等形式的屬性. 特別是 C++ 標準樣板程式庫中, 它運用得特別多. 例如 libc++ 中關於 std::vector 的成員函式 begin 的實作是這樣的 :

template <class _Tp, class _Allocator>
inline _LIBCPP_INLINE_VISIBILITY
typename vector<_Tp, _Allocator>::iterator
vector<_Tp, _Allocator>::begin() _NOEXCEPT
{
    return __make_iter(this->__begin_);
}

其中, _LIBCPP_INLINE_VISIBILITY 是一個巨集, 它會首先被替換為 _LIBCPP_HIDE_FROM_ABI, 然後 _LIBCPP_HIDE_FROM_ABI 被替換為 _LIBCPP_HIDDEN _LIBCPP_INTERNAL_LINKAGE. 這裡有兩個巨集, 一個是 _LIBCPP_HIDDEN, 另一個是 _LIBCPP_INTERNAL_LINKAGE. _LIBCPP_HIDDEN 會被替換為 __attribute__ ((__visibility__("hidden"))), _LIBCPP_INTERNAL_LINKAGE 會被替換為 __attribute__ ((internal_linkage)). 這兩個便是屬性.

在 C++ 11 之前, 不同的編碼器對 Attribute 有不同的表示方法, C++ 11 正式將 Attribute 標準化.

更新紀錄 :

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

1. 基本概念

C++ 11 統一了 Attribute 的語法, 包括在 [[]] 之內的就是屬性 (attribute). 它有四種標識的方法 :

  • [[attribute]];
  • [[attribute(arg1, arg2, ...)]];
  • [[attribute1, attribute2, ...]];
  • [[using name-space::attribute]].

其中, arg 是屬性的引數, name-space 是屬性所在的名稱空間.

屬性本身對於程式設計師來說是可有可無的, 因為它的出現只是為了方便編碼器進行優化, 一般的程式設計師根本用不到甚至不知道它的存在. 但是對於程式庫的作者或者想要深入閱讀程式庫的那些人來說, 這個特性是需要了解的. 理論上來說, 任意陳述式之前都標識屬性, 但是某些屬性本身是有一定的適用範圍的, 使用在錯誤的地方可能會產生編碼錯誤.

1.1 自訂型別標識

[[attribution1]] class / struct / union / enum [[attribution2]] Foo {} [[attribution3]] c [[attribution4]], d [[attribution5]] 使用了類別或者列舉宣告了用戶自訂型別 Foo, 並且同時宣告了兩個變數 cd. 其中, [[attribution1]] 作用於變數 cd; [[attribution2]] 作用於 Foo 的宣告; [[attribution3]] 作用於 Foo 的定義; [[attribution4]][[attribution5]] 分別作用於變數 cd.

1.2 變數宣告標識

[[attribution1]] int [[attribution2]] * [[attribution3]](* [[attribution4]] *[[attribution5]] f [[attribution6]])() [[attribution7]], e [[attribution8]] 過於複雜, 可以簡化為 int * (**f)(), e. 它宣告了一個指向函式指標的指標 f 和一個 int 型別的變數 e. 其中, [[attribution1]] 作用於變數 fe; [[attribution2]] 作用於函式的回傳型別 int 部分; [[attribution3]] 作用於函式的回傳型別 int * 部分; [[attribution4]] 作用於變數 f 對應型別的第二級指標 (即指向函式指標的指標); [[attribution5]] 作用於變數 f 對應型別的第一級指標 (即函式指標); [[attribution6]] 作用於變數 f; [[attribution7]] 作用於函式 f (即通過 f 呼叫函式(**f)()); [[attribution8]] 作用於變數 e.

1.3 建構子標識

設類別 Foo 中存在自訂的預設建構子 Foo(), 那麼可以使用屬性將預設建構子標識為 Foo::Foo [[attribution1]] () [[attribution2]] {...}. 其中, [[attribution1]] 作用於名稱 Foo; [[attribution2]] 作用於預設建構子 Foo().

1.4 陣列宣告標識

int [[attribution1]] a[2] [[attribution2]]; 宣告了一個具有兩個元素的 int 陣列. 其中, [[attribution1]] 作用於型別 int 部分; [[attribution2]] 作用於陣列 a (其型別為 int [2]).

1.5 型別別名標識

using Foo [[attribution]] = struct {}; 宣告了一個用戶自訂類別 Foo. 其中, [[attribution]] 作用於名稱 Foo.

2. 標準化的 Attribute

每一家編碼器都提供了不少屬性, 有些屬性可能只是某一家編碼器所專有的. 因此, 這些帶有專有屬性的程式碼被其它編碼器編碼的時候, 其它編碼器可能因為不認識這些屬性, 從而擲出編碼警告或者編碼錯誤. 但是在 C++ 17 之後不會產生編碼錯誤, 因為 C++ 17 為屬性引入了未知屬性忽略的特性. 到 C++ 20 為止, 被標準化的屬性還不多.

2.1 [[noreturn]]

當函式以擲出例外情況或著終結程式的方式終結, 可以為函式標識 [[noreturn]] 屬性. 除了方便編碼器優化之外, 還可以抑制編碼器的警告. 這個屬性在 C++ 11 被引入.

#include <exception>

[[noreturn]]
void throw_error(const char *str) {
    throw std::runtime_error(str);
}
int func(int i) {
    if(i == 0) {
        return 0;
    }else {
        throw_error("runtime error");
    }
}

如果沒有為函式 throw_error 標識 [[noreturn]] 的話, 像 Apple Clang 就會擲出編碼警告 : warning: control may reach end of non-void function [-Wreturn-type].

2.2 [[carries_dependency]]

這個屬性在 C++ 11 被引入, 但是因為涉及多執行緒, 所以此處暫時不作介紹.

2.3 [[deprecated(reason)]]

[[deprecated(reason)]] 這個屬性表示編碼者並不建議程式碼使用者使用某一個名稱 (變數, 型別定義甚至是函式). 其中, reason 是一個表明原因的字面值字串, 原因將在編碼警告處顯示. 當不需要表明原因的時候, 可以直接簡寫為 [[deprecated]], 省略掉原因. 這個屬性是 C++ 14 引入的.

enum E {
    A,
    B [[deprecated]]
};

[[deprecated("deprecated variable")]]
int a {E::B};       // Apple Clang warning: 'B' is deprecated [-Wdeprecated-declarations]

[[deprecated]]
void func() {
    ::a = E::A;
}
void func2() {
    func();     // Apple Clang warning : 'func' is deprecated [-Wdeprecated-declarations]
}

有一些比較好的 IDE 會在補全名稱的時候或著分析之後會給出提示或者警告, 以下圖片來自 Jetbrains CLion :

Figure 1-1. CLion 給出的警告
Figure 1-2. CLion 給出的遺棄名稱補全提示

2.4 [[fallthrough]]

[[fallthrough]] 表示禁止編碼器對 switch 中某些故意下落的陳述式擲出編碼警告. 這個屬性在 C++ 17 引入.

#include <cstdlib>

void func(int c) {
    switch(c) {
        case 0:
            func(++c);
            [[fallthrough]];
        case 1:
            func(c + 10);
            [[fallthrough]];
        case 3:
            break;
        default:
            std::abort();
    }
}

Code 4 中, 如果沒有標識 [[fallthrough]], 那麼部分編碼器會對 case 0case 1 中下落的陳述式擲出編碼警告.

2.5 [[nodiscard]]

[[nodiscard]] 表示某一個函式的回傳值不應該被忽略, 如果被忽略, 編碼器應擲出編碼警告. 這個屬性在 C++ 17 被引入, 在 C++ 20 中被加強. 在 C++ 20 中, [[nodiscard]] 還可以接受一個原因 [[nodiscard(reason]], 以便於編碼器在擲出警告的時候, 同時告知原因. 類似於 [[deprecated(reason)]], reason 是一個表明原因的字面值字串.

[[nodiscard]]
int f(int x) {
    return x + 42;
}
int main(int argc, char *argv[]) {
    f(0);       // warning
}

2.6 [[maybe_unused]]

[[maybe_unused]] 是告知編碼器某些名稱雖然沒有被使用, 但是這是我們故意為之的, 以避免某些編碼器的警告. 這個屬性在 C++ 17 被引入.

對於某些變數或著函式 (包含成員函式以及成員變數), 可能出現宣告以及定義之後沒有被使用的情況. 這樣的情況下, 某些編碼器可能會給出警告. 此時, 需要給函式或著變數加上屬性 [[maybe_unused]] 以避免編碼警告.

2.7 [[likely]][[unlikely]]

[[likely]][[unlikely]] 是用於某些條件陳述式的優化, 告知編碼器哪一些條件更可能被滿足, 哪一些條件被滿足的機率較低. 這兩個屬性在 C++ 20 中引入.

void func(int c) {
    switch(c) {
        case 0:
            func(c + 1);
            break;
        [[likely]]      // case 1 比其餘分支更可能被選擇
        case 1:
            func(c + 10);
            break;
        [[unlikely]]        // case 3 被選擇的機率比其它分支都小
        case 3:
            func(c - 100);
            break;
        default:
            break;
    }
}
void func(int c) {
    if(c > 0) [[likely]] {
        // 這個分支更可能被選中
        // ...
    }else if(c == 0) {
        // ...
    }else [[unlikely]] {
        // 這個分支選中的機率較小
        // ...
    }
}

[[likely]][[unlikely]] 不應該在同一個陳述式中出現, 否則會產生編碼錯誤. 濫用這兩個屬性中的任意一個, 都可能編碼器對程式碼產生負面優化, 導致程式效能降低.

2.8 [[no_unique_address]]

[[no_unique_address]] 用於標識類別內非靜態成員變數或者非位元欄位成員變數不需要唯一的記憶體位址. 這個屬性在 C++ 20 中被引入. 具有 [[no_unique_address]] 屬性的成員通常其型別可作為基礎類別或著僅僅被用於類別的尾部填充 :

#include <iostream>

struct empty {};
struct X {
    int i;
    empty e;
};
struct Y {
    int i;
    [[no_unique_address]]
    empty e;
};

int main(int argc, char *argv[]) {
    using std::cout;
    using std::endl;
    cout << sizeof(empty) << endl;      // 輸出結果 : 1
    cout << sizeof(X) << endl;      // 輸出結果 : 8
    cout << sizeof(Y) << endl;      // 輸出結果 : 4
}

3. 名稱空間

某些屬性可能位於編碼器設定的某個名稱空間中, 在 C++ 17 之前, 若要訪問這些屬性, 則應該顯式地寫出名稱空間 : [[gnu::attr1, gnu::attr2, gnu::attr3(arg1, arg2)]]. C++ 17 為屬性引入了 using 宣告, 這樣只需要第一個屬性顯式地寫出名稱空間即可 : [[using gnu::attr1, attr2, attr3(arg1, arg2)]].