C++初心者がヘッダーファイルに感じた「なぜ?」を掘り下げてみた

に公開6

前提

この記事を書くのはC++初心者です。そしてこういう技術記事を書くのも初めてです。
ヘッダーファイルについて勉強していたら思ったより面白かったので、誰かに共有したくなってまとめました。
間違いがあっても「初心者の戯言」として優しく指摘してもらえると嬉しいです。
また、関連して面白い記事があれば、ぜひコメントで教えてください!


疑問

C++では、複数のファイルに分けてコードを書く場合、他ファイルの関数を使うには「使う側のファイル」でプロトタイプ宣言をしなければいけません。
しかし関数が何百、何千と増えてきたら毎回宣言を書くのは現実的ではありません。
そこで登場するのがヘッダーファイルです。
でも、ふと疑問が浮かびました。

「.cppファイルをそのままincludeすればいいのでは?」

これが今回のテーマです。


.cppをincludeするとどうなるのか

1. 多重定義が発生する

たとえば以下のようなコードを考えてみます。

// main.cpp
#include <iostream>
#include "foo.cpp"

int main() {
    foo();
}

// foo.cpp
#include <iostream>

void foo() {
    std::cout << "foo!!" << std::endl;
}

プリプロセッサは #include "foo.cpp" の行を、実際に foo.cpp の中身に置き換えます。
つまり、main.cppの中にfoo関数の定義がそのまま貼り付けられることになります。
このとき、もしビルド時に foo.cpp 自体も別途コンパイルされると、
foo() の定義が2つ存在することになり、「多重定義エラー」が発生します。


2. 翻訳単位の崩壊

では、もしfoo.cppをコンパイル対象から外せば動くのでは?
……動くには動きます。が、C++の根本的な仕組みが崩れてしまいます。

🧩 翻訳単位(translation unit)

コンパイラが1回のコンパイルで扱うソースコードの最小単位。

#includeなどのプリプロセス後の状態を指します。

通常、.cppはそれぞれが独立した「翻訳単位」としてコンパイルされる前提で設計されています。
しかし.cppを他の.cppにincludeすると、それが別の翻訳単位に埋め込まれる形になります。
その結果:

  • foo.cppを変更するとmain.cppも再コンパイルが必要になる
  • デバッグ時に「どのファイルのどの関数が悪いのか」追いづらくなる
    といった問題が起こります。
    つまり、「独立してコンパイルできる」というC/C++の大きな強みが失われてしまうのです。

C++の設計思想に触れてみる

C++は、大規模開発を支えるためのC言語の拡張として生まれました。
C言語では、1つの大きなプログラムを小さく分け、個別にコンパイルして後でリンクする仕組みが取られていました。
これが「翻訳単位」という仕組みです。
C++はこの構造をそのまま引き継ぎました。
つまり、他の.cppの関数を使うためには「宣言だけ共有する」必要があります。
この“宣言だけ共有”を実現するための仕組みが、ヘッダーファイルです。

C++は“古い仕組みを守りつつ拡張した言語”だからこそ、

.h + .cppという構成が必要になったのです。


他の言語ではどうしてるの?

「でも、C++以外ではこんな面倒な仕組みを聞いたことがない…」
そう思って他の言語も調べてみました。

🟦 Java

宣言と定義を同じファイルに書きます。

public class Greeter {
    public void greet() {
        System.out.println("Hello");
    }
}

他のファイルでは import 文で参照します。
宣言と定義を分けるという概念がそもそも存在しません。


🟨 Python

# greeter.py
def greet():
    print("Hello")

# main.py
import greeter
greeter.greet()

動的型付け言語なので、型情報や宣言を明示する必要がありません。
importするだけで関数を呼び出せます。


🟩 C#

namespace Utility {
    public class Greeter {
        public static void Greet() {
            Console.WriteLine("Hello");
        }
    }
}

C#ではビルド時にソースコードを「アセンブリ(DLL)」にまとめます。
参照時はDLLを読み込むだけでOK。
依存関係の管理はIDE(Visual Studioなど)が自動で行います。


🟦 Go

// greeter.go
package greeter

func Greet() {
    fmt.Println("Hello")
}

Goコンパイラはパッケージ構造を理解しており、
importだけで依存解決を行える仕組みです。


そしてC++の答え:Modulesの登場

C++20では、ついにModulesという仕組みが導入されました。
ヘッダーではなくmodule#includeではなくimportで記述します。

// util.ixx
export module util;
export void greet();

// util.cpp
module util;
#include <iostream>
void greet() {
    std::cout << "Hello from module!";
}

// main.cpp
import util;
int main() {
    greet();
}

まだ勉強中ですが、ヘッダーと同様の機能をより安全・高速に行える新しい仕組みのようです。
いつか「include地獄」が終わる日が来るかもしれません。


最後に

初心者の拙い記事を最後まで読んでくれて本当にありがとう。
moduleの仕組みをちゃんと理解できたら、またこの続きを書きたいと思います。
あなたのプログラミングの旅路が、いい冒険になりますように。✨

Discussion

dameyodamedamedameyodamedame

C++というよりCですが、#includeの役割としては宣言と実装の分離という話だと思いますよ。

ヘッダとして宣言だけを#includeするなら、ソース(.c/.cpp/.cc/.cxxなど)の内容に依存せずに書けるということです。他のモジュールに対して公開したい関数だけヘッダに宣言を入れるという使い方をします。

このように分離しておくと、例えば.so/.dllなど動的にロードされるライブラリを使ってプラグインを実装できます。分離によりバイナリレベルで差し替えることもできるということです。

また本文中では、ソースを#includeすると説明されてましたが、よほど変わったプロジェクトでなければそんなことはしません。ただしヘッダに宣言だけでなく実装もinlineで書いてしまうことが出来ます。他の言語と同様に宣言と実装は分離されなくなるということです。その場合はおっしゃるとおりビルドの時間が増え、同一性の確認まで必要になるはずです。しかしinlineで書くと最適化時の実行効率が飛躍的に上がるので、C++だと特にヘッダに実装を記述しがちで、しばしばビルド時間が問題になります。

個人的にimportにはあまり必要性を感じません。C++20で導入されましたが、今のところ主流になっていないようです。import単品では破壊的変更な割にメリットが少ないからだと思います。

kanetkanet

コメントありがとうございます!!
つまり、ヘッダに分離しておくのは「後から処理を変える必要が出てきたときにも対応できるようにするため」ということでそれがincludeの役割でもあると。
処理を付け替えれるタイプのpublic修飾子みたいな感じですかね?
inlineについて初めて知りました!
inlineでのヘッダへの実装の記述はどこまでの量なら許容なのでしょうか?

dameyodamedamedameyodamedame

処理を付け替えれるタイプのpublic修飾子みたいな感じですかね?

オブジェクト指向が目新しかった時代の感覚でいえばそういう感じですよ

inlineでのヘッダへの実装の記述はどこまでの量なら許容なのでしょうか?

例えば小さなライブラリの場合、全てをヘッダに書いてしまう、ということがよくあるというくらい許容されます

https://github.com/p-ranav/awesome-hpp

kanetkanet

githubで実例を示してくださりありがとうございます!!
まだ駆け出しの学生ということもあり私にとってオブジェクト指向は目新しいイメージが強いのですが、見慣れてくるとこのincludeとpublicの見え方はまた別のものになるのですか?

dameyodamedamedameyodamedame

私にとってオブジェクト指向は目新しいイメージが強いのですが、見慣れてくるとこのincludeとpublicの見え方はまた別のものになるのですか?

そうですね。オブジェクト指向でいえばオブジェクト単位で可視性が制御でき、C++ならpublic/protected/privateなど3段階に分かれています。includeはファイル単位でしか制御出来ず、2段階しかありません。インスタンスもないですしね。普通は比較対象にしません。

kanetkanet

たしかに可視性を考えたりインスタンスを作れるかと考えると別物ですね...
納得しました!!
いろいろ教えてくださりありがとうございます!!