C++】attribute
attribute 是 C++ 11 新引入的內容, 講述完這個特性之後, C++ 11 的內容我們就幾乎全部講解完畢了

在 C++ 11 之前, 你可能見過 __attribute__, __attribute, __declspec, #pragma 形式的屬性. 特別是當你在查看 C++ 標準程式庫的程式碼的時候, 你會發現某些函式之前會有巨集, 這些巨集我們在程式設計中不會用到. 而絕大部分巨集都是用於屬性的替換 :

【C++】attribute-Jonny'Blog
【C++】attribute-Jonny'Blog
【C++】attribute-Jonny'Blog
【C++】attribute-Jonny'Blog
【C++】attribute-Jonny'Blog

在 C++ 11 之前, 不同的編碼器對屬性有不同的表示方法. 為此, C++ 11 將其標準化. 在 C++ 11 之後, C++ 將屬性統一為 [[attribute]]. 屬性的基本語法為

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

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

理論上, 對於陳述式, 任意陳述式之前都可以放置屬性. 但是接下來介紹的某些屬性本身是有一定的適用範圍的, 使用在錯誤的地方可能會產生編碼錯誤

屬性本身對於程式設計師來說是可有可無的, 因為這只是為了方便編碼器進行優化. 一般的程式設計師根本用不到甚至不知道它的存在, 對於程式庫的作者來說, 可能才需要掌握這個特性

接下來, 我使用一些實例講述屬性可以被用在何處 :

1. [[attr1]] class / struct / union / enum [[attr2]] Foo {} [[attr3]] c [[attr4]], d [[attr5]];

  • attr1 作用於物件 cd
  • attr2 作用於類別 Foo 的定義
  • attr3 作用於型別 Foo
  • attr4 作用於物件 c
  • attr5 作用於物件 d

2. [[attr1]] int [[attr2]] * [[attr3]](* [[attr4]] *[[attr5]] f [[attr6]])() [[attr7]], p [[attr8]];

  • attr1 作用於變數 fe
  • attr2 作用於函式的回傳型別 int 部分
  • attr3 作用於函式的回傳型別 int * 部分
  • attr4 作用於變數 f 對應型別的第二級指標 (即函式指標的指標)
  • attr5 作用於變數 f 對應型別的第一級指標 (即函式指標)
  • attr6 作用於變數 f
  • attr7 作用於函式 f ((**f)())
  • attr8 作用於變數 e

3. struct Foo {Foo();};    Foo::Foo [[attr1]] () [[attr2]] {}

  • attr1 作用於名稱 Foo
  • attr2 作用於建構子 Foo()

4. int [[attr1]] a[2] [[attr3]];

  • attr1 作用於型別 int 部分
  • attr2 作用於陣列 a (其型別為 int [2])

5. using Foo [[attr]] = struct {};

  • attr 作用於名稱 Foo

每個編碼器都有不少的屬性, 不過目前被標準化的屬性還不多 (至少到 C++ 20 為止)

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

#include <exception>



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");

    }

}       //warning: control may reach end of non-void function [-Wreturn-type]

對於上述程式碼來說, 編碼器會對 func 有一分支沒有回傳值產生警告, 如果為 throw_error 加上 [[noreturn]] 屬性, 那麼就可以抑制這個警告 :

#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");

    }

}       //OK

[[carries_dependency]] : 這個屬性在 C++ 11 被引入, 但是這個屬性我們不作介紹, 需要了解的可以訪問 https://stackoverflow.com/questions/6411270/what-does-the-carries-dependency-attribute-mean

[[deprecated]] / [[deprecated(reason)]] : 這個屬性表示編碼者並不建議程式碼使用者使用某一個名稱 (變數、型別定義甚至是函式), 其中 reason 是一個表明原因的常數字串, 其型別為 const char *, 原因將在編碼警告處顯示

使用帶有這樣屬性的名稱, 編碼器會發出警告提醒使用者 :

enum E {

    A,

    B [[deprecated]]

};



[[deprecated("deprecated variable")]]

int a {E::B};       //warning: 'B' is deprecated [-Wdeprecated-declarations]



[[deprecated]]

void func() {

    ::a = E::A;

}

void func2() {

    func();     //'func' is deprecated [-Wdeprecated-declarations]

}

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

【C++】attribute-Jonny'Blog
【C++】attribute-Jonny'Blog

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

對於某一些故意下落的 case 陳述式, 某些編碼器可能會因此擲出編碼警告 :

#include <iostream>



void func(int c) {

    switch(c) {

        case 0:

            func(++c);      //warning

        case 1:

            func(c + 10);       //warning

        case 3:

            break;

        default:

            std::abort();

    }

}

case 陳述式的最後使用 [[fallthrough]] 屬性表明 break 是故意被忽略的, 從而避免警告 :

#include <iostream>



void func(int c) {

    switch(c) {

        case 0:

            func(++c);

            [[fallthrough]];        //OK

        case 1:

            func(c + 10);

            [[fallthrough]];        //OK

        case 3:

            break;

        default:

            std::abort();

    }

}

[[nodiscard]] : 這個屬性表示某一個函式的回傳值不應該被忽略, 如果被忽略, 編碼器應擲出編碼警告. 這個屬性在 C++ 17 被引入

部分函式是存在回傳值的, 並且回傳值不應該被忽略, 否則函式呼叫可能沒有意義. 例如 :

#include <vector>



using namespace std;



void func() {

    vector<int> vec;

    vec.empty();        //若忽略回傳值, 這樣的呼叫將會毫無意義. 但是此時編碼器不會擲出任何編碼警告

}

因此, 在函式之前增加這個屬性, 讓編碼器給出警告 :

[[nodiscard]]

constexpr int func() noexcept {

    return 0;

}

除了 [[nodiscard]] 之外, C++ 20 還引入了 [[nodiscard(reason)]]. 和 [[deprecated(reason)]] 一樣, reason 是一個表明原因的常數字串, 其型別為 const char *, 原因將在編碼警告處顯示

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

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

[[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]] {

        //do something...

    }else if(c == 0) {

        //do something

    }else [[unlikely]] {

            //do something...

        }

    }

}

這兩個屬性不應該在同一個陳述式中出現. 濫用這兩個屬性中的任意一個, 都可能導致程式性能下降

[[no_unique_address]] : 用於標識類別內非靜態、非位元欄位成員變數不需要唯一的記憶體位址. 這個屬性在 C++ 20 中被引入

具有這樣屬性的成員通常其型別可作為基礎類別或著僅僅被用於類別的尾部填充 :

#include <iostream>



using std::cout;

using std::endl;



struct empty {};

struct X {

    int i;

    empty e;

};

struct Y {

    int i;

    [[no_unique_address]]

    empty e;

};



int main(int argc, char *argv[]) {

    cout << sizeof(empty) << endl;      //輸出結果 : 1

    cout << sizeof(X) << endl;      //輸出結果 : 8

    cout << sizeof(Y) << endl;      //輸出結果 : 4

}

C++ 17 為屬性引入了 using 宣告 : 某些屬性可能位於編碼器設定的某個名稱空間中, 而 C++ 17 之前, 若要訪問這些屬性, 則應該顯式地寫出名稱空間 :

[[gnu::attr1, gnu::attr2, gnu::attr3(arg1, arg2)]]

這樣會使得程式碼過長, 而引入 using 宣告之後, 可能簡化程式碼, 只需要第一個屬性顯式地寫出名稱空間即可 :

[[using gnu::attr1, attr2, attr3(arg1, arg2)]]

C++ 17 還為屬性引入了未知屬性忽略. 當程式進行移植的時候, 可能會更換另一款編碼器. 但是某一些屬性僅僅是某些編碼器特有的, 如果移植之後編碼器無法識別或著並不支援其它編碼器特有的屬性, 則可能會擲出編碼錯誤. 此時, 將不得不重新修改程式碼, 以移除這些無法識別的屬性. 而 C++ 17 引入未知屬性忽略, 可以避免這個麻煩. 當編碼器遇到位置屬性的時候, 至多只會擲出編碼警告而不會導致編碼錯誤