C++のコードをヘッダに書くか、ソースに書くかについて

実現したいこと

  • C++の実行コードをヘッダに書くか、ソースに書くかについて、皆さんのご意見をうかがいたい
  • 例えば「自分はベストプラクティスを知っているので広めたい!」という意見
  • 例えば「自分はこういうケースでいつもこうしている」という意見
  • 例えば「自分はよく知らないけど、こういうツールを知っている」という意見
  • etc...

前提

  • C++11(以降)
  • 公開範囲については皆さんが想定されるケースを決めてください(言及しない場合はオープンソース)
  • 規模感については皆さんが想定されるケースを決めてください(言及しない場合は数k行程度以内)
  • 外部コードの有無については、皆さんが想定されるケースを決めてください(言及しない場合は自身がアプリから使用されるライブラリ)
  • 処理系についてはgcc9以降限定
  • プラットフォームについては限定しませんが、OSはあるものとしてください

発生している問題・エラーメッセージ

  • (A)ヘッダに書く場合は、公開したくないコードまで公開する必要がある
  • (B)ソースに書く場合は、テンプレートのインスタンス化が悩ましい
  • (C)ヘッダに書く場合は、同じものが複数展開されるため、それぞれでコードが異なりうる懸念がある

(B),(C)については以下コードで例示します

該当のソースコード

環境構築用のスクリプトです。最後にビルドして実行します。

bash

1#!/bin/sh2cat >Makefile <<EOF 3CC=g++ 4CXXFLAGS=-g -std=c++11 -Wall 5all: main main_lib main_diff 6clean: 7 rm -f *.o *.a main main_lib main_diff 8main: main.o 9main_lib: main_lib.o libsub.a 10 \$(CC) main_lib.o -L. -lsub -o main_lib 11libsub.a: libsub.a(sub_lib.o) 12main_diff: main_diff.o libsub2.a 13 \$(CC) main_diff.o -L. -lsub2 -o main_diff 14main_diff.o: main_diff.cpp sub_diff_v1.h 15 cp sub_diff_v1.h sub_diff.h 16 \$(CXX) \$(CXXFLAGS) \$(CPPFLAGS) \$(TARGET_ARCH) -c main_diff.cpp 17sub2_diff.o: main_diff.cpp sub_diff_v2.h 18 cp sub_diff_v2.h sub_diff.h 19 \$(CXX) \$(CXXFLAGS) \$(CPPFLAGS) \$(TARGET_ARCH) -c sub2_diff.cpp 20libsub2.a: libsub2.a(sub2_diff.o) 21EOF22cat >main.cpp <<EOF 23#include <iostream> 24#include "sub.h" 25int main() { 26 Hoge hoge; 27 hoge.print(std::cout); 28 return 0; 29} 30EOF31cat >main_diff.cpp <<EOF 32#include <iostream> 33#include "sub_diff.h" 34#include "sub2_diff.h" 35int main() { 36 Hoge hoge; 37 hoge.print(std::cout); 38 sub2(); 39 return 0; 40} 41EOF42cat >main_lib.cpp <<EOF 43#include <iostream> 44#include "sub_lib.h" 45int main() { 46 Hoge hoge; 47 hoge.print(std::cout); 48 return 0; 49} 50EOF51cat >sub.h <<EOF 52#pragma once 53#include <ostream> 54struct Hoge { 55 template<typename T> void print(T& out) { out << "ほげ" << std::endl; } 56}; 57EOF58cat >sub2_diff.cpp <<EOF 59#include <iostream> 60#include "sub_diff.h" 61#include "sub2_diff.h" 62void sub2() { 63 Hoge hoge; 64 hoge.print(std::cout); 65} 66EOF67cat >sub2_diff.h <<EOF 68#pragma once 69void sub2(); 70EOF71cat >sub_diff.h <<EOF 72#pragma once 73#include <ostream> 74struct Hoge { 75 template<typename T> void print(T& out) { out << "ふが" << std::endl; } 76}; 77EOF78cat >sub_diff_v1.h <<EOF 79#pragma once 80#include <ostream> 81struct Hoge { 82 template<typename T> void print(T& out) { out << "ほげ" << std::endl; } 83}; 84EOF85cat >sub_diff_v2.h <<EOF 86#pragma once 87#include <ostream> 88struct Hoge { 89 template<typename T> void print(T& out) { out << "ふが" << std::endl; } 90}; 91EOF92cat >sub_lib.cpp <<EOF 93#include <ostream> 94#include "sub_lib.h" 95template<typename T> 96void Hoge::print(T& out) { 97 out << "ほげ" << std::endl; 98} 99template void Hoge::print(std::ostream&); 100EOF101cat >sub_lib.h <<EOF 102#pragma once 103struct Hoge { 104 template<typename T> void print(T& out); 105}; 106EOF107make108./main 109./main_lib 110./main_diff

(B)/(C)の説明

まずは基本となるmain.cppとsub.hを見てください。

main.cpp

c++

1#include <iostream>2#include "sub.h"3int main() {4 Hoge hoge;5 hoge.print(std::cout);6 return 0;7}

sub.h

c++

1#pragma once2#include <ostream>3struct Hoge {4 template<typename T> void print(T& out) { out << "ほげ" << std::endl; }5};

何の変哲もない「ほげ」って出力するだけのコードです。
ヘッダにコードが記述されているので、モジュールも1つだけになっている単純な例です。
自分のコードがsub.hで、これを公開すると思ってください。

(B)ソースに書く場合は、テンプレートのインスタンス化が悩ましい

sub.hの実行コードをソースに移し、(静的)ライブラリにする形態がmain_lib.cpp、sub_lib.h、sub_lib.cppになります。

main_lib.cpp

c++

1#include <iostream>2#include "sub_lib.h"3int main() {4 Hoge hoge;5 hoge.print(std::cout);6 return 0;7}

sub_lib.h

c++

1#pragma once2struct Hoge {3 template<typename T> void print(T& out);4};

sub_lib.cpp

c++

1#include <ostream>2#include "sub_lib.h"3template<typename T>4void Hoge::print(T& out) {5 out << "ほげ" << std::endl;6}7template void Hoge::print(std::ostream&);

気づきましたでしょうか?

sub_lib.cppの最終行に、テンプレートのインスタンス化用の定義が入っています。この行はstd::coutのクラスであるstd::ostreamが定義されています。これがないと main_lib.cpp 側の hoge.print(std::cout) 呼び出しでundefined referenceが発生しちゃうわけです。呼び出す側のコードで、T=std::outputstreamなメソッドを必要とするので、呼び出される側のコードでそのインスタンス化が必要になるということです。

つまり呼び出す側のコードで使いたいTが増えるごとに、呼び出される側にこの行が必要になるということになります。これが面倒だというのが(B)です。

(C)ヘッダに書く場合は、同じものが複数展開されるため、それぞれでコードが異なりうる懸念がある

説明のため、元のコードを以下のようにしています。

  • main.cpp→main_diff.cpp
  • sub.h→sub_diff_v1.h

さらに同じものを複数展開するために、

  • sub.h→sub_diff_v2.h
  • sub2_diff.h
  • sub2_diff.cpp

というのを用意しています。ようはモジュールを2つに増やし、それぞれにsub.h相当をインクルードして呼び出させるということです。
sub2側はライブラリにして、main側からリンクします。

main_diff.cpp

c++

1#include <iostream>2#include "sub_diff.h"3#include "sub2_diff.h"4int main() {5 Hoge hoge;6 hoge.print(std::cout);7 sub2();8 return 0;9}

sub_diff_v1.h

c++

1#pragma once2#include <ostream>3struct Hoge {4 template<typename T> void print(T& out) { out << "ほげ" << std::endl; }5};

sub_diff_v2.h

c++

1#pragma once2#include <ostream>3struct Hoge {4 template<typename T> void print(T& out) { out << "ふが" << std::endl; }5};

sub2_diff.h

c++

1#pragma once2void sub2();

sub2_diff.cpp

c++

1#include <iostream>2#include "sub_diff.h"3#include "sub2_diff.h"4void sub2() {5 Hoge hoge;6 hoge.print(std::cout);7}

各.cppファイルでインクルードしてるファイルがsub_diff.hになっていますが、これは_v1/_v2というサフィックスがついたファイルをmake注にコピーして作成しています。何を意図しているかというと、main_diff側と、libsub2.a側で異なるバージョンのsub_diff.hを使ったケースの再現です。そのままビルドすると、main_diff側のコードに一本化されて動きますが、最適化させるとインライン展開されてそれぞれ異なるコードで動作します。

最後に

C++を使ってる人なら誰もが一度はどうしようかと悩むところだと思いますが、あっちを立てればこっちが立たずみたいな状況なので、意見を集めてみてもいいかなと思って、書いてみました。参考までに私は公開ソースなら全部ヘッダにしていて、非公開なら規模次第でソースにしてます。テンプレートは限定的な使い方にします。

※勘違いの指摘なども歓迎です

コメントを投稿

0 コメント