Chrome-base源碼分析(2)之Macros模塊
Author:Once Day Date:2024年6月29日
漫漫長路,才剛剛開始…
全系列文章請查看專欄: 源碼分析_Once-Day的博客-CSDN博客
參考文檔:
- macros - Chromium Code Search
- Chrome base 庫詳解:工具類和常用類庫_base::repeatingcallback-CSDN博客
- For Developers (chromium.org)
- 手動代碼中包含C++20 __VA_OPT__錯誤的類函數宏-騰訊云開發者社區-騰訊云 (tencent.com)
文章目錄
- Chrome-base源碼分析(2)之Macros模塊
- 1. 概述
- 2. concat.h
- 3. if.h
- 4. is_empty.h
- 5. remove_parens.h
1. 概述
chrome-base源碼中macros模塊是一個比較簡單的模塊,定義了一些簡單的宏,有五個源文件,下面一一介紹。
2. concat.h
源碼如下:
// A macro that expands to the concatenation of its arguments. If the arguments
// are themselves macros, they are first expanded (due to the indirection
// through a second macro). This can be used to construct tokens.
#define BASE_CONCAT(a, b) BASE_INTERNAL_CONCAT(a, b)// Implementation details: do not use directly.
#define BASE_INTERNAL_CONCAT(a, b) a##b
這個非常基礎的關鍵字拼接宏,例如下面所示:
TEST(MacrosTest, Concat) {auto a = BASE_CONCAT(10, 20);std::cout << "a: " << a << std::endl;EXPECT_EQ(a, 1020);auto b = BASE_CONCAT(5000, 6000);std::cout << "b: " << b << std::endl;EXPECT_EQ(b, 50006000);
}
>>> a: 1020
>>> b: 50006000
BASE_CONCAT(10, 20)
會輸出1020,這是可以作為源碼字面量的值,并不是"10" + "20" = "1020"
這種字符串拼接。
3. if.h
源碼如下:
// Given a `_Cond` that evaluates to exactly 0 or 1, this macro evaluates to
// either the `_Then` or `_Else` args. Unlike a real conditional expression,
// this does not support conditions other than `0` and `1`.
#define BASE_IF(_Cond, _Then, _Else) \BASE_CONCAT(BASE_INTERNAL_IF_, _Cond)(_Then, _Else)// Implementation details: do not use directly.
#define BASE_INTERNAL_IF_1(_Then, _Else) _Then
#define BASE_INTERNAL_IF_0(_Then, _Else) _Else
這段代碼定義了一個名為BASE_IF的宏,用于實現編譯期的條件選擇功能。
宏接受三個參數:_Cond
、_Then
和_Else
。_Cond
必須是一個計算結果為0或1的表達式。
根據_Cond
的值,宏會將其展開為_Then
或_Else
參數的內容。
宏的實現依賴于兩個內部宏BASE_INTERNAL_IF_1
和BASE_INTERNAL_IF_0
,它們分別選擇_Then
和_Else
參數。
通過巧妙的宏拼接,BASE_IF
能夠在編譯期根據條件選擇代碼,而不會產生運行時開銷。
例如下面所示:
TEST(MacrosTest, If) {auto a = BASE_IF(1, 10, 20);std::cout << "a: " << a << std::endl;EXPECT_EQ(a, 10);auto b = BASE_IF(0, 100, 200);std::cout << "b: " << b << std::endl;EXPECT_EQ(b, 200);
}
>>> a: 10
>>> b: 200
4. is_empty.h
源碼如下:
// A macro that substitutes with 1 if called without arguments, otherwise 0.
#define BASE_IS_EMPTY(...) BASE_INTERNAL_IS_EMPTY_EXPANDED(__VA_ARGS__)
#define BASE_INTERNAL_IS_EMPTY_EXPANDED(...) \BASE_INTERNAL_IS_EMPTY_INNER(_, ##__VA_ARGS__)
#define BASE_INTERNAL_IS_EMPTY_INNER(...) \BASE_INTERNAL_IS_EMPTY_INNER_EXPANDED(__VA_ARGS__, 0, 1)
#define BASE_INTERNAL_IS_EMPTY_INNER_EXPANDED(e0, e1, is_empty, ...) is_empty
這段代碼定義了一個名為BASE_IS_EMPTY
的宏,用于檢查宏是否被傳遞了參數。如果宏調用時沒有傳遞任何參數,則展開為1,否則展開為0。
宏的實現依賴于幾個內部宏:
- BASE_INTERNAL_IS_EMPTY_EXPANDED: 對傳入的參數進行展開,并在前面添加一個下劃線。
- BASE_INTERNAL_IS_EMPTY_INNER: 在展開后的參數列表前添加固定的參數。
- BASE_INTERNAL_IS_EMPTY_INNER_EXPANDED: 根據參數個數選擇結果,如果只有固定參數則說明原宏調用時沒傳參數,返回1,否則返回0。
通過這種巧妙的宏展開和參數匹配,BASE_IS_EMPTY能夠在編譯期判斷宏調用時是否傳遞了參數。下面是一些示例:
BASE_IS_EMPTY() // 展開為 1
BASE_IS_EMPTY(a) // 展開為 0
BASE_IS_EMPTY(a, b) // 展開為 0
這個宏常用于其他宏定義中,用于根據傳參情況生成不同的代碼,或者進行靜態斷言檢查宏參數等。比如可以寫一個字符串連接的宏:
#define CONCAT(a, ...) \BASE_IF(BASE_IS_EMPTY(__VA_ARGS__), a, CONCAT_INNER(a, __VA_ARGS__))// 當只傳一個參數時直接返回,多個參數時遞歸拼接
在windows上編譯時,需要傳入/Zc:preprocessor
參數,以確保C++預編譯器的行為和GCC一致。
windows上默認行為比較特殊,如下:
// windows行為
BASE_IS_EMPTY(a, b, c, d)
>>> BASE_INTERNAL_IS_EMPTY_EXPANDED(a, b, c, d)
>>> BASE_INTERNAL_IS_EMPTY_INNER(_, a, b, c, d)
>>> BASE_INTERNAL_IS_EMPTY_INNER_EXPANDED(_, a, b, c, d, 0, 1)
e0 = _, a, b, c, d //因為windows默認把__VA_ARGS__當成整體了,這與GCC行為存在差異
e1 = 0
e2 = 1
GCC的默認行為如下:
// GCC行為
BASE_IS_EMPTY(a, b, c, d)
>>> BASE_INTERNAL_IS_EMPTY_EXPANDED(a, b, c, d)
>>> BASE_INTERNAL_IS_EMPTY_INNER(_, a, b, c, d)
>>> BASE_INTERNAL_IS_EMPTY_INNER_EXPANDED(_, a, b, c, d, 0, 1)
e0 = _
e1 = a # GCC上, 默認是按照展開位置來抉擇
e2 = b
......
雖然GCC的行為是正常展開了變量,參數和位置能一一對應,但是依舊不滿足邏輯,如下:
// BASE_IS_EMPTY() 應該返回 1
BASE_IS_EMPTY() => BASE_INTERNAL_IS_EMPTY_INNER(_) => BASE_INTERNAL_IS_EMPTY_INNER_EXPANDED(_, 0, 1)
>>> 所以is_empty == 1
// BASE_IS_EMPTY(a, b, c, d) 應該返回0
BASE_IS_EMPTY(a, b, c, d) => BASE_INTERNAL_IS_EMPTY_INNER_EXPANDED(_, a, b, c, d, 0, 1)
>>> 所以is_empty == b, 這并不符合預期
所以需要對命令稍加改造:
// 在Windows上使用msvc時, 需要/Zc:preprocessor參數, 否則宏處理會有問題
// reference: https://stackoverflow.com/questions/77700691/getting-va-opt-to-be-recognized-by-visual-studio
// 對于多個參數的情況, __VA_OPT__會將逗號和參數一起處理, 然后通過#__VA_ARGS__轉換成一個字符串// A macro that substitutes with 1 if called without arguments, otherwise 0.
#define BASE_IS_EMPTY(...) BASE_INTERNAL_IS_EMPTY_EXPANDED(__VA_ARGS__)
#define BASE_INTERNAL_IS_EMPTY_EXPANDED(...) \BASE_INTERNAL_IS_EMPTY_INNER("_" __VA_OPT__(,) #__VA_ARGS__)
#define BASE_INTERNAL_IS_EMPTY_INNER(...) \BASE_INTERNAL_IS_EMPTY_INNER_EXPANDED(__VA_ARGS__, 0, 1)
#define BASE_INTERNAL_IS_EMPTY_INNER_EXPANDED(e0, e1, is_empty, ...) is_empty
這里通過"_" __VA_OPT__(,) #__VA_ARGS__
來操作,將__VA_ARGS__
當成整體變成字符串,那么就有:
BASE_IS_EMPTY(a, b, c, d) => BASE_INTERNAL_IS_EMPTY_INNER_EXPANDED("_", "a, b, c, d", 0, 1)
>>> is_empty == 0, 符合預期
5. remove_parens.h
源碼如下:
// A macro that removes at most one outer set of parentheses from its arguments.
// If the arguments are not surrounded by parentheses, this expands to the
// arguments unchanged. For example:
// `BASE_REMOVE_PARENS()` -> ``
// `BASE_REMOVE_PARENS(foo)` -> `foo`
// `BASE_REMOVE_PARENS(foo(1))` -> `foo(1)`
// `BASE_REMOVE_PARENS((foo))` -> `foo`
// `BASE_REMOVE_PARENS((foo(1)))` -> `foo(1)`
// `BASE_REMOVE_PARENS((foo)[1])` -> `(foo)[1]`
// `BASE_REMOVE_PARENS(((foo)))` -> `(foo)`
// `BASE_REMOVE_PARENS(foo, bar, baz)` -> `foo, bar, baz`
// `BASE_REMOVE_PARENS(foo, (bar), baz)` -> `foo, (bar), baz`
#define BASE_REMOVE_PARENS(...) \BASE_IF(BASE_INTERNAL_IS_PARENTHESIZED(__VA_ARGS__), BASE_INTERNAL_ECHO, \BASE_INTERNAL_EMPTY()) \__VA_ARGS__#define BASE_INTERNAL_IS_PARENTHESIZED(...) \BASE_IS_EMPTY(BASE_INTERNAL_EAT __VA_ARGS__)
#define BASE_INTERNAL_EAT(...)
#define BASE_INTERNAL_ECHO(...) __VA_ARGS__
#define BASE_INTERNAL_EMPTY()
這段代碼定義了一個名為BASE_REMOVE_PARENS的宏,用于移除宏參數最外層的一對括號(如果有的話)。如果參數沒有被括號包圍,則宏展開后的結果與原參數相同。
宏的實現利用了之前提到的BASE_IS_EMPTY和BASE_IF宏,以及一些輔助的內部宏:
- BASE_INTERNAL_IS_PARENTHESIZED: 判斷參數是否被括號包圍。它將參數傳遞給BASE_INTERNAL_EAT宏,如果參數有括號,那么括號內的內容會被BASE_INTERNAL_EAT"吃掉",導致BASE_IS_EMPTY的結果為1,否則為0。
- BASE_INTERNAL_EAT: 接受任意參數,但不做任何事情。
- BASE_INTERNAL_ECHO: 原樣返回傳入的參數。
- BASE_INTERNAL_EMPTY: 不接受任何參數,也不返回任何內容。
BASE_REMOVE_PARENS的實現可以分為兩步:
- 使用BASE_INTERNAL_IS_PARENTHESIZED判斷參數是否有括號。
- 根據第一步的結果,使用BASE_IF選擇BASE_INTERNAL_ECHO(有括號)或BASE_INTERNAL_EMPTY(無括號),并將其展開。
最后,將原始的__VA_ARGS__附加在展開的結果之后。如果參數有括號,那么展開的空宏會移除最外層括號,否則原始參數不變。
下面是一個示例演示:
BASE_REMOVE_PARENS((foo))
>>> BASE_IF(BASE_INTERNAL_IS_PARENTHESIZED((foo))), BASE_INTERNAL_ECHO, BASE_INTERNAL_EMPTY()) (foo)
>>> BASE_IF(BASE_IS_EMPTY(BASE_INTERNAL_EAT ((foo))), BASE_INTERNAL_ECHO, BASE_INTERNAL_EMPTY()) (foo)
//這里__VA_ARGS__外面存在括號,則會執行BASE_INTERNAL_EAT宏,從而變成空,條件選擇BASE_INTERNAL_ECHO
>>> BASE_INTERNAL_ECHO (foo)
>>> foo // 去掉了外面一層括號