C 與 C++ 前置處理器

最後更新: 2023-06-09 08:19:02 +0800 +0800
原文以英文撰寫。您正在閱讀AI翻譯版本。

前置處理器會在 C 或 C++ 剖析器看到宣告、型別、命名空間或多載之前,轉換前置處理記號。它會展開引入檔案、評估條件式指令,並執行巨集替換。應將其輸出視為產生的原始碼,因為語言層級的作用域與型別檢查無法修復不安全的展開結果。

引入檔案搜尋是建置工具的契約 #

#include "project/header.hpp" 與 #include <library/header> 識別的是標頭名稱記號,而不是語言標準所規定的檔案系統路徑。標頭名稱至原始檔案的對應方式(包括引號與角括號的搜尋順序)由實作定義。工具鏈一般會針對引號形式搜尋引入檔案所在目錄與引號專用路徑,而角括號標頭則從設定的系統路徑開始搜尋;實際順序請查閱編譯器文件與建置命令。

對 GCC 與 Clang,可使用以下命令檢查目前生效的引入檔案搜尋清單:

bash
printf '' | c++ -E -x c++ -v -

依照專案慣例,專案標頭使用引號名稱,已安裝或標準函式庫的標頭則使用角括號名稱。應在適當的建置目標中設定第三方相依項目,而不是加入涵蓋範圍廣泛的全域 -I 路徑。-isystem 的搜尋位置不同,也可能改變診斷的處理方式,因此不能與 -I 互換。

每個專案標頭都需要防止重複引入的策略。傳統的標頭防護可跨相容實作移植:

cpp
#ifndef PROJECT_WIDGET_HPP
#define PROJECT_WIDGET_HPP

class Widget;

#endif

#pragma once 獲得廣泛支援,但不屬於 ISO C 或 C++ 標準。只有在支援的工具鏈可接受此相依性時才應使用。

巨集替換包含數個階段 #

對於函式式巨集的呼叫,巨集引數會在替換前展開,但參數與 # 或 ## 搭配使用時除外。替換清單接著會重新掃描,以便進一步展開。目前正在展開的巨集暫時無法遞迴展開,因而避免直接的無窮遞迴。

c
#define STRINGIZE_IMPL(x) #x
#define STRINGIZE(x) STRINGIZE_IMPL(x)
#define VERSION 42

const char* version = STRINGIZE(VERSION); // "42"

第一層會將展開後的 VERSION 傳給實作巨集;直接使用 #VERSION 會產生 "VERSION"。字串化會依照前置處理器的空白與逸出規則保留記號拼寫。使用 ## 拼接記號時,結果必須構成一個有效的前置處理記號。

c
#define JOIN_IMPL(a, b) a ## b
#define JOIN(a, b) JOIN_IMPL(a, b)

int JOIN(worker_, VERSION); // worker_42

應謹慎使用產生的識別符。拼接而成的名稱會讓一般程式碼搜尋無法找到宣告,並可能在遠離巨集呼叫的位置產生診斷。

條件式編譯與功能測試 #

條件式指令會在巨集替換後針對整數常數運算式運作。應明確測試組態巨集,並讓平台選擇邏輯靠近需要它的邊界。

cpp
#if defined(_WIN32)
#  include <windows.h>
#elif defined(__unix__)
#  include <unistd.h>
#else
#  error "Unsupported platform"
#endif

對於語言功能,應在選定的語言模式有公開相關巨集時,使用 __cpp_concepts 等標準化功能測試巨集。對於函式庫功能,應引入相關標頭,並在可用時測試對應的 __cpp_lib_* 巨集(來自 <version>)。__has_include 可在支援此運算子的實作上選用可選標頭,但找到標頭並不能證明其 API 或 ABI 符合所需版本。

對不可能成立的組態使用 #error。若某種組合可在編譯前偵測,應優先由建置系統發出診斷,因為建置系統掌握目標平台、編譯器與相依項目版本。

安全的巨集邊界 #

只要語言能表達相關操作,應優先使用 constexpr 變數、行內函式、範本、列舉與具作用域的型別。它們能保留型別檢查、作用域、多載解析與偵錯器可見性。

若仍有必要使用巨集,必須以括號包住每個運算式參數及完整的運算式結果:

c
#define SQUARE(x) ((x) * (x))

括號無法讓重複求值變得安全:SQUARE(index++) 會遞增兩次。巨集必須記載每個引數是否只求值一次,而呼叫端不應傳入帶有副作用的運算式,除非該行為符合預期。

對會展開成陳述式的巨集,使用單一陳述式模式:

c
#define LOG_FAILURE(message) \
    do {                    \
        log(message);       \
    } while (0)

此模式讓 if 陳述式可將巨集呼叫視為單一陳述式。它不會提供 C++ 作用域、型別、例外安全性或同步。應將巨集限制在可攜性、條件式編譯或記號產生等邊界,因為只有這些位置需要其文字替換特性,且能將影響控制在局部範圍。

評論

在 GitHub 上閱讀或評論(發表評論需要登入)