HOWTO · C++

C++ 中的 #pragma once:包含保護與可攜性

使用 #pragma once 或包含保護防止 C++ 標頭重複包含,並依工具鏈選擇適當方法。

#pragma once 是要求實作在同一翻譯單元中最多處理標頭檔一次的前處理器指示詞。若專案支援的所有編譯器都實作它,請將它放在一般 C/C++ 標頭檔開頭。若需要標準可攜性、必須支援未知編譯器,或已有專案慣例,請使用名稱唯一的 #ifndef/#define 包含保護。兩者都不會限制標頭檔在整個程式中只能使用一次。

在宣告之前放置 #pragma once

// config.hpp
#pragma once

class Config {
public:
    int port() const { return 8080; }
};

它不需要分號。前處理器會在編譯前處理 #include,所以此指示詞會阻止在建立一個翻譯單元時再次處理相同的標頭文字。GCC、Clang 與 MSVC 都廣泛實作它,但它不是 ISO C/C++ 標準指示詞。請參考 GCC 的一次性標頭文件說明,並確認專案實際承諾支援的工具鏈。

驗證直接與間接包含

此已驗證的三檔案範例會透過 server.hpp,以及由 main.cpp 直接兩條路徑包含 config.hpp

// server.hpp
#pragma once
#include "config.hpp"

class Server { Config config_; };
// main.cpp
#include "server.hpp"
#include "config.hpp"

int main() {
    return Config{}.port() == 8080 ? 0 : 1;
}

將三個檔案存到相同目錄後執行:

g++ -std=c++17 -Wall -Wextra main.cpp -o app
./app

config.hpp#pragma once 時,不會有終端輸出,./app 以狀態 0 結束。範例已用 g++ (Ubuntu 15.2.0-16ubuntu1) 15.2.0 驗證。只刪除 config.hpp 的該行後,GCC 會以狀態 1 結束,並回報直接包含與先前間接包含造成的 Config 重新定義。這是按翻譯單元生效,並未個別在 MSVC 或 Clang 測試。

使用標準包含保護

// config.hpp
#ifndef EXAMPLE_CONFIG_HPP
#define EXAMPLE_CONFIG_HPP

class Config {
public:
    int port() const { return 8080; }
};

#endif  // EXAMPLE_CONFIG_HPP

第一次包含會定義巨集,後續包含會略過其中的文字。選擇由專案、目錄及檔名組成且具描述性的唯一名稱。CONFIG_H 可能與其他標頭衝突;含有 __ 或以底線加大寫字母開始的識別字保留給實作。

#include 是文字包含:在檢查 C++ 宣告之前,前處理器會用標頭文字取代該指示詞。範例中 config.hpp 先經 server.hpp 進入,接著由 main.cpp 直接進入;沒有保護時,編譯器會得到兩個 Config 定義。因此保護應放在標頭本身,而非只放在某個碰巧包含它的原始檔。這是編譯錯誤,不同於分別編譯檔案產生的連結錯誤。

選擇機制

情況 選擇 原因
所有目標編譯器都實作 #pragma once #pragma once 簡短且沒有保護巨集衝突。
公開函式庫、未知編譯器或嚴格可攜性 包含保護 使用標準前處理器指示詞。
已有儲存庫慣例 原有慣例 保持標頭易於維護。
有意多次包含的標頭,例如 X-macro 清單 預設兩者都不用 重複包含正是目的。

不要預設在每個標頭同時加入兩種機制:一種通常已足夠,編譯器也能辨識常規保護。不要聲稱普遍的建置速度優勢;需要時應測量。#pragma once 依賴實作辨識檔案身分,因此別名、產生的檔案、網路檔案系統或特殊包含路徑可能是邊界條件。包含保護避開該問題,但巨集必須唯一。

標頭保護無法解決的問題

保護按翻譯單元運作,無法修正所有多重定義連結錯誤或 One Definition Rule (ODR) 問題。標頭中的非 inline 一般函式定義可能在每個 .cpp 產生外部定義;將一般定義放在 .cpp,或在設計需要時使用 inline 或樣板。它也不能解決兩個型別都需要完整定義的循環依賴:對指標或參考使用前向宣告,或移動實作。C++20 模組是另一種機制,import 不會以文字包含標頭。

摘要

可接受擴充支援時使用 #pragma once,需要標準可攜性時使用唯一的包含保護。驗證直接與間接路徑,並分別診斷 ODR、循環、刻意重複包含及模組問題。