C++ 17 fallthrough、nodiscard、maybe_unused 屬性

C++ 17 新增三個標準屬性(Attribute):

本文會依序介紹這三個屬性。

[[fallthrough]]

在 C/C++ 語言中,switch 述句通常會有若干個 case 與一個 default。在進入一個 casedefault 之後,如果沒有以 break 述句或其他述句跳出 switch 述句,程式會繼續執行接續的 casedefault

#include <iostream>

int main() {
  int x = 0;
  std::cin >> x;

  int result = 0;
  switch (x) {
    case 2:
      result++;
    case 0:
      result++;
    default:
      result++;
  }

  std::cout << result << std::endl;
}

舉例來說,如果上面的 x 等於 2,進入 case 2 之後會一路執行 case 2result++case 0result++defaultresult++

然而這常是程式錯誤或安全漏洞的源頭,因此許多 C++ 編譯器會提供額外的警告選項。如果我們使用的編譯器是 GCC 或者 Clang,我們能以 -Wimplicit-fallthrough 開啟警告:

$ g++ -std=c++17 fallthrough_example.cpp -Wimplicit-fallthrough
fallthrough_example.cpp: In function ‘int main()’:
fallthrough_example.cpp:10:17: warning: this statement may fall through
[-Wimplicit-fallthrough=]
           result++;
           ~~~~~~^~
fallthrough_example.cpp:11:9: note: here
         case 0:
         ^~~~
fallthrough_example.cpp:12:17: warning: this statement may fall through
[-Wimplicit-fallthrough=]
           result++;
           ~~~~~~^~
fallthrough_example.cpp:13:9: note: here
         default:
         ^~~~~~~

如果這段程式的確需要接續執行之後的 casedefault,我們能加上 [[fallthrough]] 屬性以關閉警告:

#include <iostream>

int main() {
  int x = 0;
  std::cin >> x;

  int result = 0;
  switch (x) {
    case 2:
      result++;
      [[fallthrough]];  // Added
    case 0:
      result++;
      [[fallthrough]];  // Added
    default:
      result++;
  }

  std::cout << result << std::endl;
}

[[fallthrough]] 屬性必須加在「此函式中能從此 case(或 default)跳至下一個 case(或 default)」的位置。在程式碼中 [[fallthrough]] 屬性與下一個 case(或 default)不一定是相鄰的兩列。我們可以看另一個複雜的例子:

#include <iostream>

int main() {
  int x = 0;
  int y = 0;
  std::cin >> x >> y;

  int result = 0;
  switch (x) {
    case 2:
      if (y % 2 == 0) {
        result++;
        [[fallthrough]];  // #1
      } else {
        break;  // #2
      }
    default:
      result++;
  }

  std::cout << result << std::endl;
}

在上面的例子裡,我們在 #1 加上 [[fallthrough]] 以明確表明繼續執行 default 的意圖。而在 #2 我們已經用 break 述句離開 switch 述句,因此我們不用加上 [[fallthrough]]

[[nodiscard]]

在一些情況下,函式的編寫者會想要「提醒」函式的呼叫者必須檢查或使用函式的回傳值。C 語言的 scanf 函式就是典型的例子:

#include <stdio.h>

int main() {
  int x;
  scanf("%d", &x);
  printf("%d\n", x);
}

如果 scanf(x) 發生錯誤,x 可能會是未定義的數值。如果我們沒有檢查 scanf() 的回傳值,後續的計算都會有問題。

作為函式的編寫者,我們能以 [[nodiscard]] 提醒函式呼叫者檢查回傳值:

#include <stdio.h>

[[nodiscard]] int read_int(int &value) {
  return scanf("%d", &value);
}

int main() {
  int x, y, z;

  read_int(x);  // #1: Trigger warning

  if (read_int(y)) {  // #2: Properly checked
    printf("%d\n", y);
  }

  (void)read_int(z);  // #3: Explicitly discarded
}

在上面的範例中,我們忽略第一個函式呼叫的回傳值(#1),因此編譯器會印出警告訊息。第二個函式呼叫(#2)展示了正確的使用方法。如果我們很清楚的知道我們要捨棄 [[nodiscard]] 函式的回傳值,我們能用 (void) 轉型關閉 [[nodiscard]] 警告。

[[nodiscard]] 屬性也能套用於 structclassenum 上。如果一個 structclassenum 帶有 [[nodiscard]] 屬性,且它是一個函式的回傳型別,則該函式視同帶有 [[nodiscard]] 屬性。例如:

#include <stdio.h>

enum [[nodiscard]] ErrorCode {
  OK,
  IO_ERROR,
};

ErrorCode read_int(int &value) {
  return scanf("%d", &value) == 1 ? OK : IO_ERROR;
}

int main() {
  int x;
  read_int(x);
}

上述範例中,雖然 read_int 函式本身沒有 [[nodiscard]] 屬性,但是因為它的回傳型別 ErrorCode[[nodiscard]] 屬性,所以編譯器會為 read_int(x) 印出警告訊息。

[[maybe_unused]]

許多 C/C++ 編譯器能幫我們找出「未使用變數」,並印出警告訊息,讓我們清理這些非必要的程式碼。

然而,在一些情況下,這些「未使用變數」並不是真的沒用。舉例來說,有時候使用這些變數的程式碼是被 #ifdef#endif 包起來,在特定組態下才會發生作用。例如:

#include <iostream>

int test(int a, int b, int c) {
  int result = a + b;

#ifdef ENABLE_FEATURE_C
  result += c;
#endif

  return result;
}

int main() {
  std::cout << test(1, 2, 3) << std::endl;
}

如果我們以 -Wunused-Wunused-parameter 編譯,我們會看到以下警告訊息:

$ g++ -std=c++17 maybe_unused.cpp -Wunused -Wunused-parameter
maybe_unused.cpp: In function ‘int test(int, int, int)’:
maybe_unused.cpp:3:28: warning: unused parameter ‘c’ [-Wunused-parameter]
 int test(int a, int b, int c) {
                            ^

如果想要關閉警告訊息,我們能加上 [[maybe_unused]] 屬性:

#include <iostream>

int test(int a, int b, [[maybe_unused]] int c) {  // Modified
  int result = a + b;

#ifdef ENABLE_FEATURE_C
  result += c;
#endif

  return result;
}

int main() {
  std::cout << test(1, 2, 3) << std::endl;
}

參考資料

  • P0188R0, P0188R1: Wording for [[fallthrough]] attribute
  • P0189R0, P0189R1: Wording for [[nodiscard]] attribute
  • R0212R1: Wording for [[maybe_unused]] attribute
  • P0068R0: Proposal of [[unused]], [[nodiscard]] and [[fallthrough]] attributes

Note

如果你喜歡這篇文章,請追蹤羅根學習筆記粉絲專頁。最新文章都會在粉絲專頁發佈通知。