C++のヘッダーファイルについて「.h は古いのか」「.hpp はいつ使うのか」「#pragma once とインクルードガードはどちらが良いのか」という疑問は、プロジェクト規模が大きくなるほど重要になります。
本稿では拡張子とガード手法の位置づけ、具体的な使い分け、現場でのベストプラクティス、そしてC++20モジュールへの移行観点までを体系的に解説します。
C++ ヘッダーファイルの.hは古い?.hppや#pragma onceの位置づけと要点
キーワード:.h と .hpp の違い/#pragma once と インクルードガード/C++ ベストプラクティス
歴史的にはC言語の名残で .h が一般的でしたが、C++専用APIやテンプレート中心の設計を明示するために .hpp も広く使われています。
拡張子は仕様として機能に差を生まないため、選択は「意味づけ」と「チーム規約」によって決まります。
一方、重複インクルード対策は .h/.hpp とは別の話で、従来のインクルードガードか #pragma once
のどちらか(または併用)を採用します。
以下の表は要点の整理です。
- 拡張子の意味づけ
- .h: C/C++共通・C互換の公開インターフェースに適合
- .hpp: C++専用・テンプレート/ヘッダオンリーに適合
- 重複インクルード対策
#pragma once
: 簡潔・速いが非標準(実装依存)- インクルードガード: 標準的・確実だが記述量多め
.h と .hpp の違いと使い分け(C互換かC++専用か)
.h(C/C++共通)を使うケース:C互換API、公開ヘッダ、extern “C”
CとC++の両方から利用されるABI安定なインターフェースは .h が適しています。
C++からCの関数を呼ぶ場合は extern "C"
を条件付きで付与し、名前修飾を抑制します。
例:C互換の公開API(.h + extern “C”)
/* c_api.h — C/C++ 共通の公開ヘッダ */
/* インクルードガードの例(標準的) */
#ifndef C_API_H_
#define C_API_H_
#ifdef __cplusplus
extern "C" {
#endif
/* C側に公開する安定なABI */
int c_add(int a, int b);
#ifdef __cplusplus
} /* extern "C" */
#endif
#endif /* C_API_H_ */
/* c_api.c — C実装 */
#include "c_api.h"
int c_add(int a, int b) {
return a + b;
}
// main.cpp — C++からC APIを呼ぶ
#include <iostream>
#include "c_api.h"
int main() {
std::cout << "sum=" << c_add(3, 4) << std::endl;
return 0;
}
sum=7
C互換のヘッダは、長期にわたりABIを変えにくいため安定性が重視されます。
struct
のレイアウトや例外非送出の契約などもCの期待に合わせると安全です。
.hpp(C++専用)を使うケース:テンプレート、ヘッダオンリー、内部向けライブラリ
テンプレートやインライン、constexpr
の定義は翻訳単位から可視である必要があるため、ヘッダに実装を含める設計が一般的です。
C++専用APIであることを明示するために .hpp を用いると、利用者の理解が早くなります。
例:テンプレートを含むC++専用ヘッダ(.hpp)
// math_util.hpp — C++専用(テンプレート/inline中心)
// #pragma once の例(簡潔)
#pragma once
#include <type_traits>
namespace util {
// 非負数の二乗根の簡易チェック付きテンプレート
template <class T>
constexpr T square(T x) {
static_assert(std::is_arithmetic_v<T>, "T must be arithmetic");
return x * x;
}
// inline 変数(C++17+)
inline constexpr int version = 1;
} // namespace util
// use_math.cpp
#include <iostream>
#include "math_util.hpp"
int main() {
std::cout << "square(3)=" << util::square(3) << ", version=" << util::version << '\n';
}
square(3)=9, version=1
拡張子の選び方:チーム規約・ツール互換性・混在プロジェクトでの指針
拡張子はコンパイラではなくツールチェーンや読者の期待を動かします。
現実的には次の基準で統一するとよいです。
- 混在(C/C++)プロジェクト
- C公開API: .h
- C++専用ライブラリ: .hpp(テンプレートやヘッダオンリーが中心なら特に有効)
- 純C++プロジェクト
- チーム規約に依存。Google/Chromiumは .h を好み、Boostは .hpp を広く採用という文化差があります。どちらでもよいが統一を優先。
- ツール互換性
- IDEのテンプレート、コード生成、スニペット、lint設定が拡張子に依存することがあります。CIやコード検索のパターンも合わせて調整します。
拡張子に意味を持たせるなら「.h=公開/互換」「.hpp=C++専用/実装を含む」といったルールが理解しやすいです。
#pragma once と インクルードガードの違いと使い方
#pragma once の利点と注意点(可搬性、シンボリックリンク、重複検出)
#pragma once
は単一行で重複インクルードを防げるため、可読性とビルド時間の点で好まれます。
多くの主要コンパイラ(GCC/Clang/MSVC/ICC)がサポートし、仮想パスの正規化やデバイスIDとinode(相当)の比較で「同一ファイル」を高速に検出します。
注意点としては以下があります。
- 非標準であること(とはいえ事実上のデファクト)。極端な移植性要求では避ける選択もあります。
- 同一ファイルを別経路で参照(シンボリックリンク、ネットワークマウント、ケース違いなど)した場合の「同一性判定」がファイルシステムやコンパイラの実装差に影響される可能性があります。近年はほぼ解決されていますが規約上は留意点です。
インクルードガードのベストプラクティス(命名規則、衝突回避、標準性)
インクルードガードはプリプロセッサ標準に基づく最も確実な方法です。
命名は衝突回避と規約遵守(予約識別子回避)の観点で行います。
- 命名例
- PROJECT_SUBSYSTEM_HEADERNAME_HPP_INCLUDED
- 大文字スネークケース+接尾語
_INCLUDED
など
- 避ける命名
- 先頭がアンダースコア+大文字(例
_FOO
)や二重アンダースコア(__FOO
)は予約領域に抵触する可能性があるため避けます。
- 先頭がアンダースコア+大文字(例
例:インクルードガードのテンプレート
#ifndef EXAMPLE_COMPONENT_HPP_INCLUDED
#define EXAMPLE_COMPONENT_HPP_INCLUDED
// ヘッダ本体
#endif // EXAMPLE_COMPONENT_HPP_INCLUDED
併用は必要か?現実的な推奨と企業コード規約の例
実務では次の3通りが見られます。
- 1)
#pragma once
のみ(Microsoft社内や多数のモダンC++プロジェクト) - 2) インクルードガードのみ(Google/Chromium/LLVM/Qtなどの大規模OSS規約)
- 3) 併用(
#pragma once
+ インクルードガード、片方が無効でも保険になる)
どれも成立しますが、組織の要求に合わせましょう。
可搬性最重視なら「ガードのみ」、開発速度重視で主要コンパイラ固定なら「pragmaのみ」、混在エコシステムでは「併用」も現実解です。
Boostは .hpp + インクルードガード、Googleは .h + インクルードガードの文化が有名です。
C++ ヘッダーファイルのベストプラクティス
依存関係の最小化と前方宣言(includeの削減とビルド時間短縮)
ヘッダは最小限の依存に留め、可能なら前方宣言で参照に置き換えます。
完全型が必要な場面(継承、メンバとして値保持、sizeof
利用など)でのみインクルードします。
#pragma once
#include <memory>
#include <string>
// 前方宣言で依存を削減
namespace repo { class UserRepository; }
class UserService {
public:
explicit UserService(std::shared_ptr<repo::UserRepository> repo);
std::string get_user_name(int id) const;
private:
std::shared_ptr<repo::UserRepository> repo_; // ポインタ/参照なら前方宣言で十分
};
#include "user_service.hpp"
#include "user_repository.hpp" // 実装側で完全型が必要
UserService::UserService(std::shared_ptr<repo::UserRepository> repo) : repo_(std::move(repo)) {}
std::string UserService::get_user_name(int id) const { return "user#" + std::to_string(id); }
これにより不要な再コンパイルを抑え、ビルド時間が改善します。
include-what-you-use
の活用も有効です。
ヘッダと実装の分離、PIMPLによるABI安定化
ABI安定性や非公開依存の隠蔽が必要な公開ライブラリではPIMPL(実装隠蔽)を検討します。
// 安定ABI向け
#pragma once
#include <memory>
class Widget {
public:
Widget();
~Widget(); // 実装はcppへ
Widget(Widget&&) noexcept; // 例外仕様もABIの一部
Widget& operator=(Widget&&) noexcept;
void draw() const;
private:
struct Impl; // 前方宣言
std::unique_ptr<Impl> p_; // 実装詳細は非公開
};
// 実装詳細はここだけが知る
#include "widget.hpp"
#include <iostream>
struct Widget::Impl {
int state = 42;
void draw_impl() const { std::cout << "state=" << state << '\n'; }
};
Widget::Widget() : p_(std::make_unique<Impl>()) {}
Widget::~Widget() = default;
Widget::Widget(Widget&&) noexcept = default;
Widget& Widget::operator=(Widget&&) noexcept = default;
void Widget::draw() const { p_->draw_impl(); }
テンプレート・inline・constexprはヘッダ/非テンプレートはソースへ
- テンプレート、
inline
関数、constexpr
は定義をヘッダに置いてODRを満たします。 - 非テンプレートの重い実装や依存の多いコードは .cpp に分離してコンパイル単位を小さく保ちます。
- ヘッダオンリーは配布容易・最適化しやすい一方で、ビルド時間増加やODR違反リスクに注意します。
ヘッダ内の規約:名前空間、インクルード順、プリコンパイルヘッダの活用
- 名前空間はトップレベルから順序立てて宣言し、ヘッダで
using namespace
をしないようにします。 - インクルード順は「自分の対応ヘッダ」→「C/C++標準ライブラリ」→「サードパーティ」→「ローカル」を基本にします。自分のヘッダを最初に入れると、漏れた依存を検出しやすいです。
- プリコンパイルヘッダ(PCH)は大規模プロジェクトのビルド短縮に有効です。
# CMake: プリコンパイルヘッダの設定例(CMake 3.16+)
target_precompile_headers(my_target PRIVATE
<vector>
<string>
"$<$<CXX_COMPILER_ID:MSVC>:stdafx.h>" # 既存資産との共存例
)
C++20 モジュールとの関係と移行戦略
ヘッダからモジュールへ:ビルド時間・ODR・可視性の改善点
C++20モジュールはヘッダ/プリプロセス中心のモデルを置き換える新機能で、以下の改善が期待できます。
- ビルド時間短縮:事前コンパイルされたBMI(ビルドメタ情報)の再利用
- ODR問題の抑制:マクロ漏れや重複定義の影響を隔離
- 可視性制御:エクスポート対象を明確化
例:モジュールインターフェースと利用
//モジュールインターフェース(C++20)
// コンパイラやビルドシステムにより拡張子は .ixx/.mxx/.cppm など
export module math;
export constexpr int add(int a, int b) { return a + b; }
import math;
#include <iostream>
int main() {
std::cout << "add(2,5)=" << add(2,5) << '\n';
}
add(2,5)=7
モジュールはヘッダより厳密な分離を提供しますが、現場ではヘッダライブラリ資産が膨大なため、一足飛びの移行は現実的ではありません。
現場での現実解:モジュールとヘッダの併用、コンパイラ対応状況
- 主要コンパイラ(MSVC/Clang/GCC)はモジュールを概ねサポートしていますが、ツールチェーン(ビルドシステム、静的解析、コード生成)の対応に差があります。
- 現実的には「新規コードはモジュール優先、既存はヘッダ維持」で混在させ、段階的に公開APIやビルドボトルネック箇所から移行します。
- テンプレートの大規模ライブラリ(例:ヘッダオンリー)はモジュール化により大幅なビルド短縮が見込めますが、配布形態やABI契約の整理が必要です。
プロジェクト運用:統一ルールと移行ガイド
拡張子ポリシー(.h/.hpp)の統一と混在リポジトリの扱い
- ルール例(推奨の一案)
- C互換の公開APIは .h
- C++専用・テンプレート中心は .hpp
- 実装ファイルは .cpp/.cc(チーム規約に合わせ統一)
- レビュー時は「拡張子の意味」を守っているかを確認します。過去資産に .h が混在していても、意味が明確なら無理な一括改名は避けます(履歴や外部リンクが断絶するため)。
#pragma once とインクルードガードの採用方針(標準テンプレート提示)
- 可搬性最優先の公開OSSやクロスコンパイラ環境ではインクルードガードを標準にします。
- 社内限定・対応コンパイラが限定的な場合は
#pragma once
を標準にできます。 - 折衷案として併用テンプレートを示し、過渡期は併用を許容します。
テンプレート例(併用)
// project/foo/bar.hpp
#pragma once
#ifndef PROJECT_FOO_BAR_HPP_INCLUDED
#define PROJECT_FOO_BAR_HPP_INCLUDED
// 内容
#endif // PROJECT_FOO_BAR_HPP_INCLUDED
既存コードの移行チェックリストとCI/静的解析の設定
移行時は次の観点をCIに組み込みます。
- インクルード監査
- include-what-you-use(IWYU)で過剰/不足インクルード検出
- 循環依存や私用ヘッダの公開ヘッダ混入を検知
- スタイル/Lint
- clang-tidy でヘッダガード命名、一貫した
#pragma once
使用、using namespace
禁止などを検査
- clang-tidy でヘッダガード命名、一貫した
- ビルド最適化
- PCHの効果測定、コンパイル時間レポート(-ftime-trace, /d1reportTime)
- 移行に伴う破壊的変更検知
- ABI/APIダイフ(abi-compliance-checker、clang-abi-dumper等)
clang-tidy 設定例
Checks: >
-*,
readability-*,modernize-*,
cppcoreguidelines-*,
llvm-header-guard
CheckOptions:
- key: llvm-header-guard.HeaderFileExtensions
value: "h;hpp"
- key: llvm-header-guard.DefinePrefix
value: "PROJECT_"
WarningsAsErrors: 'llvm-header-guard'
IWYU 実行例(CMake経由の一例)
# CMakeでIWYUを有効化(見つかったときのみ)
find_program(IWYU_PATH NAMES include-what-you-use iwyu)
if (IWYU_PATH)
set_property(TARGET my_target PROPERTY CXX_INCLUDE_WHAT_YOU_USE ${IWYU_PATH})
endif ()
また、拡張子の統一やガード方針の強制は、レビューの自動化(プリコミットフック、CI)で徹底すると運用コストが下がります。
まとめ
.h は「古い」わけではなく、C/C++共通の公開インターフェースに今でも適しています。.hpp はC++専用・テンプレート中心の設計を明示するのに有効です。重要なのは拡張子の意味づけとチームの統一です。
重複インクルード対策は .h/.hpp
とは独立の論点です。#pragma once
は簡潔かつ広く実装されていますが非標準、インクルードガードは冗長でも標準的で堅牢です。プロジェクトの要求(可搬性/速度/規約)に合わせて選び、必要なら併用します。
ベストプラクティスとして、依存を最小化する設計、前方宣言の活用、テンプレート/inline/constexprの配置適正化、PIMPLによるABI安定化、適切なインクルード順とPCH導入を推奨します。
C++20モジュールはヘッダモデルの課題(ビルド時間、ODR、可視性)を大幅に改善しますが、移行は段階的に進めるのが現実的です。新規はモジュール優先、既存はヘッダを維持しつつボトルネックから移行します。
最後に、規約は「正しさ」だけでなく「一貫性」が価値です。拡張子やガード方針、ツール設定を明文化し、CIで自動検査することで、チーム全体の開発体験と品質を底上げできます。