私はC++歴3年の学生趣味プログラマーです。
「C++はなぜヘッダと実装を分けなくてはならないのか/そもそも本当に分けなければならないのか」という質問です。
C++といえば、ヘッダー部と実装部を.hファイルと.cppファイルに分けることが一般的とされている言語ですが、
これは同じオブジェクト指向言語のC#やJavaにはない特徴です。
そのせいでC++使いたちは今日もcppファイルとhファイルを行ったり来たりしながらコーディングする羽目になっています。(そしてVS使いはF12とCtrl+-を得意気に連打しています。)
私にとってもそれが当たり前になって久しいですが、
時々C++を学び始めたばかりの後輩から「なぜヘッダファイルに実装を書いてはならないのか」「なぜC++は二度も同じコードを書くことを強いるのか」と質問を受けます。
私はそのたびに「実装の隠蔽化」とか「循環参照の危険が云々」とか「そもそもそういう言語なんだ」とか「コード構造の可読性」とか「未来の自分が困るんだよ」とか言ってお茶を濁してきましたが、
自分自身どうも納得しきれていません。
例えば、個人開発で、循環参照の問題やコンパイル時間の問題がないようなコード構造であれば、
ヘッダに実装を書くことに果たして問題はあるのでしょうか?
私はどうやって後輩たちを納得させ、C#ライクなC++コードを書きたがる彼らを説得したらよいでしょうか。
意見を聞かせてもらえれば嬉しいです。
参考にしたサイト一部
ヘッダファイルの謎(3)
プログラミングメモ - C++ のソースをヘッダと実装に分ける理由とか
C++ でのビルド時間を短縮するいくつかの方法
関係がありそうなキーワード
・隠蔽化、循環参照、テンプレート、inline、コンパイル
追記(15/06/04 23:34):
・「第三者利用」が問題なら「ライブラリとしての公開やチーム開発などコードの再利用がない個人開発」という前提になればヘッダに実装してしまっても構わないことになりますがいいのでしょうか。
・「コンパイル時間」が問題ならマシンスペックの向上で将来的に解決されるレベルの問題であり、あくまで"現時点では"ヘッダファイルに実装を書くのは好ましくない、という話になってしまいますがそういう理解でよろしいのでしょうか。
・結局、二度書きの手間に対して十分なメリットが確保できるか否かという話になり、好みや自己責任の話になってしまうのではと危惧しています。C++とはどんな言語なのか、なかなか思想が見えてこなくて理解が及ばないです。レイヤーの低さや速度に特徴があるが、開発効率ではC#に劣る古き業務用言語ということなのでしょうか?(あえて極端な言い方をしています)
・(なお、私自身はヘッダで実装を書くことはないです。そこそこの規模のコードを書いたりしますしヘッダと実装は分けないとやってらんないです。自分が書いたコードを読んで構造を思い出す作業は苦行ですのでそれが不要な時点で恩恵は強く感じています。ですのでこれは主観の話ではなく言語思想,言語仕様の話として回答いただければ幸いです。)
気になる質問をクリップする
クリップした質問は、後からいつでもMYページで確認できます。
またクリップした質問に回答があった際、通知やメールを受け取ることができます。
バッドをするには、ログインかつ
こちらの条件を満たす必要があります。
回答18件
0
MUSTな理由としては参考リンクの
プログラミングメモ - C++ のソースをヘッダと実装に分ける理由とか
でもちょっとだけ言及されていますが、実装とヘッダ(型、インターフェース、定数定義)をちゃんと分ければ実装のソースを公開ぜずにビルド済みの(静的/動的)ライブラリとヘッダだけ提供すれば第3者が利用することができます。例えばWindowsのSDKがそうですね。
それにプラグインSDKを公開しているようなソフト、例えば画像処理ソフトのAdobe Photoshopとか、統合3DソフトのAutodesk Mayaなどがあります。最近は機能拡張はJavaScriptとかPythonを組み込んだソフトも多いですが、スピードが求められる世界ではまだC/C++も健在です。
あとは商用ミドルウェア(ライブラリ提供)でもこの用件を満たさないと、ソースを公開しないでクライアントに使ってもらうことができませんね。
C/C++はコンパイルするといわゆるネイティブコード(機械語+α)を出力しますが、デバッグ用途で出力されたものを除けば、これに型情報は含まれません(structとかtypedefとか、enumの値の意味とか)。型情報は別のファイルで、それがC/C++ではヘッダファイルという位置づけです。Javaではコンパイル後はクラスファイル、C#ではアセンブリという名前ですが、これらは型情報も保持しています。だからヘッダファイルは必要ないのです。これにはデメリットもあります。型情報だけのせいではないですがネイティブコードと比べ、逆コンパイルされるとかなり元に近いソースが復元できてしまいます。
MAYな理由はNUNU_E64さんのおっしゃる通り、循環参照やコンパイル時間です。納得しきれてないということですが、それはヘッダと実装をちゃんと分けた結果痛い目にあわなかった証拠かもしれません。
参考までに大規模C++ソフトウェアデザインという本を紹介しておきます。例えばコンパイル時間ですが、この本にはビルドに1週間かかるようになってしまった話が載っています。概説は「一戸建て住宅と超高層ビルでは建築に必要な技能や資格がまったく異なる」というような話から始まります。コンパイル時間の問題は言ってみれば高層ビルで深刻になる問題です。なので個人開発でしたらそこまで気にしなくてもよいと思います。しかしもし将来後輩たちと高層ビルに取り組むのを見据えているのなら、いまのうち気を使っておいても損ではありません。
投稿2015/06/04 12:17
編集2015/06/04 12:22総合スコア1151
0
短くてつまらない答え:C++言語仕様としてそのように定義されており、違反するプログラムの動作結果は全く保証されないから。
C++では、関数や変数の「宣言」と「定義」は厳密に区別されます。プログラム全体を通じて同じ「宣言」が何回出現しても良いですが、同じ「定義」(この質問の文脈では"実装"に相当)は1回だけしか存在してはなりません。これは**One Definition Rule(ODR)**とよばれ、同じ定義を複数含むプログラムはODR違反により未定義動作、つまりプログラムの実行結果が意味不明な出力となったり、コンパイルエラーになったりと、何が起こるかは全くわかりません。
ヘッダファイルに「定義」が書いてある場合、そのヘッダファイルをincludeするすべてのコンパイル単位(cppファイル)に同一の「定義」が複製されて登場します。これらのコンパイルされたファイルをリンクして1つのプログラムを生成するとき、そのプログラムには同一の「定義」が複数含まれることになります。これは先のODR違反となるため、正しく動作する保証のないプログラムが出来上がります。
このルールの例外として、テンプレート(template
)定義や、インライン関数(inline
)定義はヘッダファイルに書いてもOKです。何がインライン関数となるかについては、少々ややこしいルールがあるので下記も参考にしてください。
lang
1// (複数cppファイルからincludeされるヘッダファイルと仮定) 2 3class Foo { 4 int m1, m2, m3; 5public: 6 // ここではメンバ関数の宣言のみ 7 int getM1(); 8 int getM2(); 9 10 // OK: クラス定義内でメンバ関数を定義した場合、暗黙にインライン関数となる 11 int getM3() { 12 return m3; 13 } 14}; 15 16// OK: クラス定義外で明示的にinlineキーワード指定すればインライン関数となる 17inline int Foo::getM1() { 18 return m1; 19} 20 21// NG:クラス定義外でinlineキーワードがなければ非インライン関数となる 22int Foo::getM2() { 23 return m2; 24}
ちなみに、ODR違反になるか否かは「インライン関数として扱われるか」という観点のみが関与します。インライン関数が実際にインライン展開されるかどうかは、ODRの議論とは無関係です。inline
キーワードはコンパイラへの最適化ヒント情報という側面ももり、こちらは単にコンパイラによる最適化性能の話です。最近のまともなコンパイラであれば、非インライン関数であってもインライン展開を行ったり、インライン関数であっても通常の関数呼び出しになったりします。
投稿2015/06/06 01:56
編集2015/06/09 12:57総合スコア6191
バッドをするには、ログインかつ
こちらの条件を満たす必要があります。
2015/06/08 14:42
2015/06/09 02:09
2015/06/09 12:58
2015/06/10 15:12
0
ヘッダとソースに分ける最大の理由は分割コンパイルのためではないでしょうか。
極端な例ですが、以下のようなコードがあったとして
main.cpp
lang
1#include <iostream> 2#include "func.h" 3 4int main() 5{ 6 std::cout << func(1, 2) << std::endl; 7 return 0; 8}
func.h
lang
1inline int func(int a, int b) 2{ 3 return a + b; 4}
func 関数の中身をちょっと変更しただけでも main.cpp をコンパイルし直す必要がありますが、main.cpp は iostream もインクルードしているので、実際には 17555 行ものコードのコンパイルが必要です(-E はプリプロセスされたコードを出力するオプション)。
$ g++ -E main.cpp | wc -l 17555
そこで func.h を次のようにヘッダとソースに分割して、
func.h
lang
1int func(int a, int b);
func.cpp
lang
1int func(int a, int b) 2{ 3 return a + b; 4}
さらに main.cpp と func.cpp を分割コンパイルすれば、
$ g++ -c main.cpp $ g++ -c func.cpp $ g++ main.o func.o
この後 func 関数の中身を修正する必要があったとしても main.cpp をコンパイルし直す必要がありません。
$ g++ -c func.cpp $ g++ main.o func.o
ヘッダに実装を書いていたがために、func 関数を少しいじるのに 17555 行ものコードのコンパイルが必要でしたが、ヘッダとソースを分けたことで、たった 4 行(func.cpp の行数)のコンパイルだけで済むようになりました(*.o のリンクが必要なので単純計算で早くなっているわけではありませんが)。
投稿2015/06/04 11:34
総合スコア4516
バッドをするには、ログインかつ
こちらの条件を満たす必要があります。
0
他の方の回答を見て、「コンパイルが長い」という表現があいまいだと思ったので実体験と具体的な数字を出したいと思います。
むかし600KB, 150ファイル程度のC++プログラムの保守をしていたのですが、5年前の最新型のPCでフルビルドが1時間半ぐらいかかっていました。
フルビルドが長い事自体は、まあ、仕方がないんですけども、問題は保守業務で発生する1~2ファイルに対する変更と、そのファイルのコンパイルと、リンクです。
まずコンパイルがですね、変更した1~2ファイルに依存しているファイルもコンパイルすることになるんですけども、これが下手な include の作りになっていると芋づる式にどんどん増えるんですよ。
何故か10分ぐらいかかったりします。
ちなみにリンクも30分ぐらいかかったりしたんですけど、これはC++のクラスの仕様の影響などが大きいので省きます。
それで、常に可能な限り、依存関係が増えないように作るようになります。
前方宣言を利用するなどは当然で、依存関係のためにクラスの構造を変えた事もあります。
正しいオブジェクト指向が好きな人からは嫌がられましたが、引換に得られたものは1回のビルドで数分~数十分の節約でした。
保守業務では、ビルドとテストを1日に何十回とやるので、毎日数時間の影響がありました。
このようにビルドの時間を現実的な長さに収める、という1点においても、実装を別のファイルに書くことには大きな意義があります。
投稿2015/06/05 00:48
総合スコア133
バッドをするには、ログインかつ
こちらの条件を満たす必要があります。
2015/06/05 06:02
2015/06/05 08:10
2015/06/05 08:37
2015/06/06 05:37
2015/06/06 15:08
0
追記に対して。
コンパイル時間を気にしないのであれば、全部ヘッダに書いてもいいですよ。
ただし 循環参照等の問題が出るので、その場合は ヘッダも作ります
私の場合は
最初はヘッダに全部かいて、動作の確認をし
循環参照等の問題が出た時、リファクタリングの時に、ヘッダと定義をわけます。
作法としては、ヘッダと実装部をわけ、pImplイデオムを使い、プライベートメンバも隠蔽化&コンパイル高速化
するのが 綺麗ですので、教育するなら 綺麗なのを教える方が良いとおもいますが。
C++でテンプレートバリバリで作ると、テンプレートはヘッダにしか書けなくなるので
望まなくても、ヘッダに大量のコードを書く事になります。
投稿2015/06/04 18:41
総合スコア144
バッドをするには、ログインかつ
こちらの条件を満たす必要があります。
0
ヘッダと実装を分けないでコードディングすること発想は良いことだと思いますが
ヘッダファイルにコードを記述するのはキルディだと個人的に思います。なので
「cppファイルのほうにすべてをまとめるつもりで、ヘッダファイルの依存を減らすために構成を工夫するようにコードを書けば、ワンランク上のコードが記述できる」と説得すれば良いと思います。
投稿2015/06/04 10:24
退会済みユーザー
総合スコア0
バッドをするには、ログインかつ
こちらの条件を満たす必要があります。
0
循環参照などで、どうしてもcppに出す必要は出てきますが、前提のとおりそれを排除するとしても
ヘッダに書くとコンパイルに時間がかかります
理由は、C言語はコンパイル時にファイルの依存関係を見てますが
実装部と分離する事で依存関係を減らしているからです
ヘッダと実装が別れている理由は、C言語はビルド時に全てのシンボルのサイズを計算する必要がありますが
今の importのような便利な機能だと、シンボル解決に何度も同じソースを解析する必要があるので
ヘッダや前方宣言を使い、1回でシンボルの解決をさせる目的だと聞いています。
現在のPCは速くなったので #importも可能なんですが、C言語は下位互換性を維持するため
#includeは消えずに残ると思います
#importの追加も 検討中だと聞きました
とりあえず現状は、ヘッダと実装を分けない場合は
コンパイルが遅くなります。
投稿2015/06/04 09:56
総合スコア144
バッドをするには、ログインかつ
こちらの条件を満たす必要があります。
0
C++も時代が進み、コンパイル時処理やtemplateメタプログラミングを活用した型制約プログラミングが当たり前になってきています。
そのなかで、ヘッダーオンリーライブラリはごく普通のものとなっています。
ex.) https://github.com/bolero-MURAKAMI/Sprout
これはtemplateやconstexprといった機能を使うためにヘッダーオンリーにせざるをえない状況になっているためです。
ODRやリンケージについての指摘がありましたが、
http://fimbul.hateblo.jp/entry/2014/12/11/000123
を参考にしてください。これらはヘッダーオンリーにすることの支障にはなりません(外部リンゲージをもつ変数が必要(mutexのための変数など)な場合を除く)
コンパイル時間については
少なくともVSでは関数定義を無理してヘッダーに書かないようにする必要はなさそうだ - Qiita
以前検証したことがありますが、コンパイラがある程度コンパイル結果をキャッシュしていたりするので、多少あちこちからincludeされているヘッダーを書き換えてもそこまで時間がかからなかたりします。
もっとも、C/C++の#include
は単なるコピペに過ぎないので、コンパイル時間を増大させていることは間違いなく、ずっとmoduldeというものの導入をC++標準化委員会は議論していますが、まだ当分標準入りしそうにはありません。はやく入ってコンパイル時間がboooost!しないようになるといいなぁ。
投稿2016/09/20 15:00
総合スコア5852
バッドをするには、ログインかつ
こちらの条件を満たす必要があります。
0
NUNUさんの言っている意見の中では「そもそもそういう言語なんだ」が最も近いと言えると思います。
コンパイラの性能も向上し、コンパイラごとにやっていることも異なるので、今からする話は必ずしもそうだ、といえるものではありません。
C++では実装ファイル(.cpp)単位でオブジェクトファイルを生成します。共有したヘッダファイル(.h)の情報はC++の場合個別のオブジェクトファイルに含まれています。
C++ではインスタンス化する時、宣言部を参照してクラスサイズを計算し、サイズに基づいてメモリを確保しています。(そしてその後コンストラクタがあれば呼び出しが行われます。)後は確保したメモリアドレスを宣言部に合わせて名前解決し、実装を探してリンクしているのです。
クラスサイズの話については、以下を参照すると詳しくのっています。
https://www.microsoft.com/japan/msdn/vs_previous/visualc/techmat/feature/jangrayhood/
実装部をヘッダファイルに含めてしまうと、includeする全ての実装ファイルでその実装が取り込まれてしまいます。コンパイラの性能向上によって必ずしも最終的なアプリケーションのサイズが増えるとは言えませんが、少なくともオブジェクトファイルにはコピーされた実装が含まれることになるでしょう。
C++では実装ファイル間でインスタンスの受け渡しを行う場合、メモリのアドレスを共有しているにすぎません。クラスのメソッドが実装ファイルに存在している場合は、クラス名とメソッド名から名前解決を行い、実装部を含むオブジェクトファイルを探し、そこにジャンプしてくれるようにリンカがプログラムをビルドします。リンカのこの機能を外部参照を解決するとか表現します。
ヘッダファイルに全て書いた場合、コンパイラがどのようにプログラムを構築しているか、想像することは非常に難しくなります。インライン展開されたのか、されていないのかもよくわからなくなりますし、ビルド手順によっては思わぬバグが発生するリスクも高まります。
JavaやC#にはVMがありますから、各ファイルをコンパイルした後名前解決できれば柔軟に対応することができます。そのため、生成するオブジェクトファイルをC++と違って整理しています。Cと互換性を保つことに重きを置かれたC++とは、どのようにファイルをリンクするか、その思想から違います。コンパイラの性能が向上しても、ヘッダファイルに全ての実装を書く事はC++にとって正しい選択にはならないでしょう。
投稿2015/06/05 09:31
編集2015/06/09 06:14総合スコア1593
バッドをするには、ログインかつ
こちらの条件を満たす必要があります。
0
Javaはインターフェースて形でhの役割をしてるのでは?
ヘッダーは定義ファイルで、設計図を書く場所です。
実装は設計図を元に作るものです。
2つをまとめてしまうと、そこでしか使えないものになります。
間違えではないですが、汎用性が全くなくなり、メンテの時に苦労します。
究極は、ヘッダーファイルだけを見れば、実装を見なくても、なにが出来るか
直に解るので、コードを読む必要がなくなります。
投稿2015/06/04 13:02
総合スコア1021
バッドをするには、ログインかつ
こちらの条件を満たす必要があります。
0
技術的な観点からは様々なご意見出ていますので、少し違った視点から書かせていただきます。
個人ユースのプログラムではJavaなどが中心になっていくのかもしれませんが、産業系のシステム開発の現場では今後もC言語、C++が主流であり続けるだろうと思います。既存システムの運用・再利用、技術者の確保などからいっても、新しい言語が取って代わるのはそうそうあることではありません。銀行系のシステムではいまだにCOBOLが生き残っていますね。
また、C言語系ではコードチェックツールの使用を義務付ける企業も増えています。(JavaやC#の世界のことは私わかりません)
「○○システムでレベル××のワーニングは出してはいけない」というようなルールを決めて、自社のSEや外注業者などに「守りなさい」と命令するわけです。
なので、あなたや後輩さんたちが今後職業としてソフトウェア開発にかかわっていくのなら、一般的に”きれい”とみなされるコーディングスタイルを身につけておくのは得策です。
>「第三者利用」が問題なら「ライブラリとしての公開やチーム開発などコードの再利用がない個人開発」という前提になればヘッダに実装してしまっても構わないことになりますがいいのでしょうか。
「第三者利用だから」、「個人開発だから」とコーディングスタイルを切り替えることができるでしょうか?
一度身についてしまった癖(特に初心者の時についてしまった癖)をやめるのは、難しいことです。
企業で新人教育などに携わってみると、「悪い癖のついた経験者より何も知らない初心者のほうが教育しやすい」のは事実です。
もう1つ。
あなたも後輩さんも学生とのことですから、あなたが一方的に「説明して納得させ」なくてもよいのでは?
疑問を持つことはよいことですが、それ以上に「自分で考える」ことも重要です。
「なぜこういう慣習が生まれたのか」後輩さんたちとディスカッションしてみるのはいかがでしょうか?
長文、失礼しました。
投稿2015/06/22 07:55
総合スコア229
バッドをするには、ログインかつ
こちらの条件を満たす必要があります。
0
主観になりますが、
ヘッダファイルに実装を書く件は邪道ではありません。
好きにコーディングすれば良いと思います。
NUNU_E64さんが後輩に説明している件は全てC++で
開発を進めた場合に発生しうるリスク回避の手法です。
やらなければならないのではなく、
どこまでリスクを事前に想定し回避するか、
開発に携わったプログラマが判断すべき要件です。
後輩達に納得させるには、
「ヘッダファイルに実装を書いてはならない」と教えるのではなく、
メリットとデメリットを明示し知識として教えるべきです。
分けてコーディングすることにメリットしか無いような言い方は
問題の解決にならないと思います。
投稿2015/06/12 16:05
総合スコア57
バッドをするには、ログインかつ
こちらの条件を満たす必要があります。
0
最近はC++を使うことはなくなったので最新の言語仕様を追えていないので間違っているかもしれませんが…。
C++と、C#やJavaなどのヘッダーと実装がわかれていない言語との差は、
C++では、クラス定義内で実装を書かれたメソッドはすべてインライン展開される(呼び出した場所にコードを展開して埋め込まれる)
のに対して
C#やJavaでは、クラス定義内で実装を書かれたメソッド(つまりは、すべてのメソッドですが…)は、
必ずひとつの関数のみ存在し、そのメソッドを呼び出したコードがあれば、常に関数の呼び出しが起きる
ことになります。
このためC++では、ヘッダーファイル内(クラス定義)でメソッドの実装を書いてしまうと、
メソッドを呼び出したコードのすべてで(ほぼ)同じ機械語に展開され、
メソッドが大きくなればなるほど最終的な実行ファイルも大きくなることになります。
(C#やJavaではこういう問題はおきない。)
それを嫌って、うん十年前はヘッダーファイル内には簡単なメソッド以外の実装は書かなかった気がします。
現在の状況はよくわかりませんが、そんな歴史もあったということで、ご参考まで。
参考URL:http://www7b.biglobe.ne.jp/~robe/cpphtml/html02/cpp02008.html
余談ですが、C++でヘッダー内でメソッドを実装すると、機械語レベルでは
関数呼び出し(スタックのpush & call)や関数からの復帰(スタックのpop & return)が起きないので実行速度があがったりします。
投稿2015/06/09 05:32
編集2015/06/09 05:38総合スコア314
バッドをするには、ログインかつ
こちらの条件を満たす必要があります。
2015/06/09 07:17
2015/06/09 10:00
2015/06/10 02:17
0
削除させていただきました。
投稿2016/08/21 10:30
編集2016/08/29 02:17退会済みユーザー
総合スコア0
バッドをするには、ログインかつ
こちらの条件を満たす必要があります。
0
可読性についてだけ、コメントさせて下さい
ヘッダに実装を書く場合も、宣言と実装を別けて書くことは出来ますよ
宣言やクラス定義だけ書いたヘッダの最後の方で、別のヘッダ(VCだと慣例的に*.inlとかの拡張子にすることもありますが実質ヘッダと同じ)
をインクルードして、そこに実装を書けばいいだけです
テンプレートだと基本的にソースに実装を書けないので、可読性を確保したいときには重宝する方法です
投稿2016/12/16 13:03
退会済みユーザー
総合スコア0
バッドをするには、ログインかつ
こちらの条件を満たす必要があります。
あなたの回答
tips
太字
斜体
打ち消し線
見出し
引用テキストの挿入
コードの挿入
リンクの挿入
リストの挿入
番号リストの挿入
表の挿入
水平線の挿入
プレビュー
質問の解決につながる回答をしましょう。 サンプルコードなど、より具体的な説明があると質問者の理解の助けになります。 また、読む側のことを考えた、分かりやすい文章を心がけましょう。
バッドをするには、ログインかつ
こちらの条件を満たす必要があります。