C++ 17 新增三個標準屬性(Attribute):
- [[fallthrough]] 用以標記接續執行下一個 switch case 的意圖。
- [[nodiscard]] 用以提醒函式呼叫者檢查回傳值。
- [[maybe_unused]] 用以標記可能不會被使用的變數宣告。
本文會依序介紹這三個屬性。
[[fallthrough]]
在 C/C++ 語言中,switch
述句通常會有若干個 case
與一個 default
。在進入一個 case
或 default
之後,如果沒有以 break
述句或其他述句跳出 switch
述句,程式會繼續執行接續的 case
或 default
:
#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 2
的 result++
、case 0
的 result++
與 default
的 result++
。
然而這常是程式錯誤或安全漏洞的源頭,因此許多 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:
^~~~~~~
如果這段程式的確需要接續執行之後的 case
或 default
,我們能加上 [[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]]
屬性也能套用於 struct
、class
或 enum
上。如果一個 struct
、class
或 enum
帶有 [[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;
}