質問をすることでしか得られない、回答やアドバイスがある。

15分調べてもわからないことは、質問しよう!

新規登録して質問してみよう
ただいま回答率
85.46%
C++11

C++11は2011年に容認されたC++のISO標準です。以前のC++03に代わるもので、中枢の言語の変更・修正、標準ライブラリの拡張・改善を加えたものです。

C++

C++はC言語をもとにしてつくられた最もよく使われるマルチパラダイムプログラミング言語の1つです。オブジェクト指向、ジェネリック、命令型など広く対応しており、多目的に使用されています。

意見交換

クローズ

16回答

3480閲覧

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

dameo

総合スコア943

C++11

C++11は2011年に容認されたC++のISO標準です。以前のC++03に代わるもので、中枢の言語の変更・修正、標準ライブラリの拡張・改善を加えたものです。

C++

C++はC言語をもとにしてつくられた最もよく使われるマルチパラダイムプログラミング言語の1つです。オブジェクト指向、ジェネリック、命令型など広く対応しており、多目的に使用されています。

1グッド

1クリップ

投稿2023/04/20 08:53

1

1

実現したいこと

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

前提

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

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

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

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

該当のソースコード

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

bash

1#!/bin/sh 2cat >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) 21EOF 22cat >main.cpp <<EOF 23#include <iostream> 24#include "sub.h" 25int main() { 26 Hoge hoge; 27 hoge.print(std::cout); 28 return 0; 29} 30EOF 31cat >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} 41EOF 42cat >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} 50EOF 51cat >sub.h <<EOF 52#pragma once 53#include <ostream> 54struct Hoge { 55 template<typename T> void print(T& out) { out << "ほげ" << std::endl; } 56}; 57EOF 58cat >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} 66EOF 67cat >sub2_diff.h <<EOF 68#pragma once 69void sub2(); 70EOF 71cat >sub_diff.h <<EOF 72#pragma once 73#include <ostream> 74struct Hoge { 75 template<typename T> void print(T& out) { out << "ふが" << std::endl; } 76}; 77EOF 78cat >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}; 84EOF 85cat >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}; 91EOF 92cat >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&); 100EOF 101cat >sub_lib.h <<EOF 102#pragma once 103struct Hoge { 104 template<typename T> void print(T& out); 105}; 106EOF 107make 108./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 once 2#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 once 2struct 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 once 2#include <ostream> 3struct Hoge { 4 template<typename T> void print(T& out) { out << "ほげ" << std::endl; } 5};

sub_diff_v2.h

c++

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

sub2_diff.h

c++

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

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

ttb👍を押しています

気になる質問をクリップする

クリップした質問は、後からいつでもMYページで確認できます。

またクリップした質問に回答があった際、通知やメールを受け取ることができます。

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

回答16

#1

SaitoAtsushi

総合スコア5466

投稿2023/04/20 11:06

(C) の事例については未定義の挙動です。 異なる翻訳単位にある同じクラス定義・インライン関数は「同じトークンの並び」で構成されることが要求されているのでそうでないときにどういう結果になるかわかりません。

しかし、仕様の範囲で書かれている限り同じものが複数の実体を持つということはありません。 最適化とは関係なく実体がひとつであることは保証されています。 実際の動作としてはリンク時に統合されるということです。 明示的実体化を複数の場所でやっている場合であってもそれらは同じものとして統合されます。

コードで説明すると以下のようなことです。

cpp

1// main.cpp 2#include <cassert> 3#include "sample.h" 4 5int main(void) { 6 // main.cpp から見える hello<int> と 7 // sample.cpp の hello<int> は同じもの。 8 // アドレスは同じ! 9 assert(hello_int_address()==hello<int>); 10}

cpp

1// sample.cpp 2#include "sample.h" 3 4void (*hello_int_address(void))(void) { 5 return hello<int>; 6}

cpp

1// sample.h 2#include <iostream> 3 4template <class T> 5void hello(void) {} 6 7void (*hello_int_address(void))(void);

未定義なのでそもそもそういうことを引き起こしてしまったらいけないとは言ってもうっかりバージョンが混在するミスをしてしまったら……ということを懸念しているのだとしてもあまり気にする意味がないと思います。

そういうミスがあったら一見して正しく動いていても問題であることには変わりないからです。 むしろミスが顕在化しやすいほうがありがたいくらいでしょう。

複数の翻訳単位から使うテンプレートは原則としてヘッダに実装を書くものだと私は考えています。

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

#2

dameo

総合スコア943

投稿2023/04/20 12:03

編集2023/04/20 12:04

#1
ありがとうございます。
コードについては、Makefileの追加と少し手直しさせてもらいました。

Makefile

1CC=g++ 2CXXFLAGS=-g -std=c++11 -Wall 3all: main 4clean: 5 rm -f *.o *.a main 6main: main.o libsample.a 7 $(CC) main.o -L. -lsample -o main 8libsample.a: libsample.a(sample.o)

cpp

1// main.cpp 2#include <cassert> 3#include "sample.h" 4 5int main(void) { 6 // main.cpp から見える hello<int> と 7 // sample.cpp の hello<int> は同じもの。 8 // アドレスは同じ! 9 assert(hello_int_address()==&hello<int>); 10}

cpp

1// sample.h 2template <class T> 3void hello() {} 4typedef void (*hello_func_type)(); 5hello_func_type hello_int_address();

cpp

1// sample.cpp 2#include "sample.h" 3 4hello_func_type hello_int_address() { 5 return &hello<int>; 6}

おっしゃることは分かりますし、実際最適化してないときはそういう挙動になることも確認しています。ただ、例えば最適化済みのバイナリ提供を受けているライブラリが、実は何か共通のC++ヘッダオンリーライブラリを使用していて、バージョンが違っていた場合、整合が取れずに不安定な挙動を示す、などの心配があるよという話なのです。

Cで実装した場合は原則ヘッダに実装を書くことがないので、原則その辺の心配がありません。C++だとその辺が少しナイーブな感じになると思うので、懸念事項として挙げた次第です。

ご意見としては、

複数の翻訳単位から使うテンプレートは原則としてヘッダに実装を書くものだと私は考えています。

コレに尽きるということなんですね。ほんといちいち手作業で追加とかやるせないし、提供形態によっては不可能だったりしますから。ただ、例えば実装したライブラリが製品でソース提供をしない場合、それは原則から外れる例外ということになります。

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

#3

SaitoAtsushi

総合スコア5466

投稿2023/04/21 00:48

それは実装をどこに書くかとは別の話 (というかスコープの大きい話) で、依存関係の中に同じライブラリの別バージョンが含まれていると破綻するという問題ですね。

破綻するのはしょうがなくても少なくとも検出はしたいといったときに実装とヘッダの対応が取れていることを確認する仕組みを持っている事例を知っています。 deflate 圧縮アルゴリズムの実装である zlib です。

初期化処理を呼び出すときにヘッダ側のバージョンナンバーを関数に渡すというごく簡単なものです。 説明するよりコードを見たほうがはやいと思うので当該箇所を示します。

https://github.com/madler/zlib/blob/04f42ceca40f73e2978b50e93806c2a18c1281fc/zlib.h#L1810-L1811

こういった仕組みが必要になる程度には問題の種として知られているということなんでしょう。


翻訳単位ごとに独立してコンパイル可能にする仕組みによって差分ビルドが簡単に出来ることやプログラムのインターフェイス (関数の型) がヘッダという形で分離されているのは好ましいものです。

しかし、それらの整合性についてのチェックがないというのはヘッダという仕組みが根本的に時代遅れとしか言いようがありません。 特にテンプレートが現れてからヘッダに実装を書かざるを得なくなってしまいヘッダが担う役割がおかしくなってしまいました。

C++ の根本的な問題点であることは認識されています。 解決策として C++20 でモジュールの概念が導入されたので利用してみてはいかがでしょうか。

ただ、期待されていたよりは出来が悪くて皆がこぞって利用し始めるというほどにはなっていません。 ヘッダの仕組みと共存できるようになっているのですが組み合わせるとヘッダで何とかしていたときよりも面倒くさくなってしまいます。 かといって既存のライブラリと共存できない仕組みにして皆でいっせいに移行するというわけにもいかないですし。

C++ は運用でなんとかする泥臭さがある言語として受け止めるしか仕方がないですね。

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

#4

dameo

総合スコア943

投稿2023/04/21 02:03

#3
zlibについては、バージョン不整合問題はCでもあるよという話はそうなのですが、それはヘッダの宣言の違いに原則留まります。当該コードについては最終的にそれが
https://github.com/madler/zlib/blob/04f42ceca40f73e2978b50e93806c2a18c1281fc/deflate.c#L252-L255
でチェックされてますね(ちゃんと見たよというだけ)。ソースに記述する関数全てでコレを先頭に入れたい気分ではあります。C++では実行コードがそのままヘッダに出てきて、インライン展開されちゃったりするので、Cに比べても分かりにくい問題になったり、うまく動いてたのに突然意味不明に落ちたり、ありえない動作をしてくれたり、調べてもソースが違ってて気づかないことになりそうです。

翻訳単位ごとに独立してコンパイル可能にする仕組みによって差分ビルドが簡単に出来ることやプログラムのインターフェイス (関数の型) がヘッダという形で分離されているのは好ましいものです。

しかし、それらの整合性についてのチェックがないというのはヘッダという仕組みが根本的に時代遅れとしか言いようがありません。 特にテンプレートが現れてからヘッダに実装を書かざるを得なくなってしまいヘッダが担う役割がおかしくなってしまいました。

おっしゃるとおりだと思います。リンク時の最適化機能をもっと良くすれば或いは…と思ってましたけど、そっちの方向には流れてないようですね。インライン展開による速度効果は小さくないと思うし、テンプレートのインスタンス化も相まって解決はなかなか難しそうです。

C++ の根本的な問題点であることは認識されています。 解決策として C++20 でモジュールの概念が導入されたので利用してみてはいかがでしょうか。

調べてみようと思います。まだ今はC++14→C++17という段階で、C++20はほぼ分かりません(そもそもガッツリ使っていたのは11を0xと呼んでた頃までで、ここ数年でようやくC++11に入門した感じなので、いつになるか分かりませんが)。

他の皆さんも遠慮は不要です。ご自由に意見してください。

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

#5

H.Hiro

総合スコア16

投稿2023/04/22 02:41

まず大前提として、「テンプレートで指定した型が異なるなら、バイナリは別になる」ということを考慮し、そこから何が解決策なのか考えるよりないように思いました。

そのうえで、「ソース側はバイナリしか公開したくない」場合に限定して考えると、インスタンス化はヘッダ側でしてもよいように思いました。テンプレートでユーザ側から指定できる型はそもそも、ユーザ側に公開されたバイナリを作っておいた型に限定されるためです。

なお私は「ソース側はバイナリしか公開したくない」という状況でC++を書いたことがないので、基本的にはテンプレートを使うクラスは実装も含めヘッダに書いているのですが、分割コンパイルができないのは悩ましいです。例えば自動的に(Makefileとかで)「テンプレートで指定した型ごとにバイナリを作る」ということができればよいのですが。

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

#6

dameo

総合スコア943

投稿2023/04/22 03:49

#5
ご意見ありがとうございます。

例えば

cpp

1// sample.cpp 2template<typename T> T plus(const T& left, const T& right) { 3 return left + right; 4} 5int main() { 6 return 0; 7}

こんなテンプレートだけある関数を未使用のままビルドしてもplus関数はコード生成されません。

bash

1$ g++ -g -Wall sample.cpp -o sample 2$ objdump -S sample | grep plus 3template<typename T> T plus(const T& left, const T& right) { 4$

これを以下のように使用すると、

c++

1// sample2.cpp 2#include <string> 3template<typename T> T plus(const T& left, const T& right) { 4 return left + right; 5} 6int main() { 7 std::cout << plus(1, 1) << std::endl; 8 std::cout << plus(1., 1.) << std::endl; 9 std::cout << plus(std::string("1"), std::string("1")) << std::endl; 10 return 0; 11}

ビルドしたときにT=int/T=double/T=std::stringなplus関数がコード生成されます。このコード生成をテンプレートのインスタンス生成と言います。

bash

1$ objdump -S sample2 | grep plus 2template<typename T> T plus(const T& left, const T& right) { 3 std::cout << plus(1, 1) << std::endl; 4 1330: e8 69 02 00 00 callq 159e <_Z4plusIiET_RKS0_S2_> 5 std::cout << plus(1., 1.) << std::endl; 6 138c: e8 2d 02 00 00 callq 15be <_Z4plusIdET_RKS0_S2_> 7 std::cout << plus(std::string("1"), std::string("1")) << std::endl; 8 1416: e8 c9 01 00 00 callq 15e4 <_Z4plusINSt7__cxx1112basic_stringIcSt11char_traitsIcESaIcEEEET_RKS6_S8_> 9 std::cout << plus(std::string("1"), std::string("1")) << std::endl; 10000000000000159e <_Z4plusIiET_RKS0_S2_>: 11template<typename T> T plus(const T& left, const T& right) { 1200000000000015be <_Z4plusIdET_RKS0_S2_>: 13template<typename T> T plus(const T& left, const T& right) { 1400000000000015e4 <_Z4plusINSt7__cxx1112basic_stringIcSt11char_traitsIcESaIcEEEET_RKS6_S8_>: 15template<typename T> T plus(const T& left, const T& right) { 16 162f: 74 05 je 1636 <_Z4plusINSt7__cxx1112basic_stringIcSt11char_traitsIcESaIcEEEET_RKS6_S8_+0x52> 17$

これらのインスタンス生成は呼び出されたときに暗黙的に実施されるので、暗黙的インスタンス生成と分類されますが、それを実施するにはテンプレートの実装定義(ヘッダ)が必要になります。実装定義が公開できない場合、ソース側に移動しないといけませんが、そのまま移動しただけでは未使用になるため、インスタンス化されません。そのため、明示的なインスタンス生成が必要になるわけです。そのコード例が質問文に示した sub_lib.cpp の7行目になります。つまり、テンプレートの明示的なインスタンス生成をライブラリ側で実施する場合、予め用意しておいたテンプレートインスタンスしか呼べないため、ユーザー指定の型を公開し、ライブラリ側にそれ用のテンプレートインスタンスを追加してもらえない限り、呼び出せない、ということになります。

一方、gccがtemplateのinstatiationに関する機能をどう実装しているかについては以下の資料があります。
https://gcc.gnu.org/onlinedocs/gcc/Template-Instantiation.html
この資料によると、Cfrontモデルなるものを使用するとなんか出来そうなのですが、現行このモデルを扱えるメジャーな処理系を知りません。

makeでエラーが起きたらエラーから、インスタンス生成コードを追加してライブラリに自動追加することは技術的には可能だと思います。ただそれが出来るためには非公開のテンプレートの定義実装が必要なので、非公開と矛盾する=不可能です。そして公開できるならヘッダに入れることになるかと思います。

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

#7

ttb

総合スコア67

投稿2023/04/25 03:50

その昔ヘッダに関数やクラスの定義を書いていて、何かの拍子にハンドリング出来なくなってしまったので(多重定義やリンクエラー等が多発)、それ以来、汎用性のために徹底してヘッダには宣言だけ書くようにしてますね・・・。
#define くらいはケースバイケースですることありますが。

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

#8

dameo

総合スコア943

投稿2023/04/25 05:15

#7
う~ん、ハンドリングできなくなる、ということは普通の状況だと流石に考えにくいです。
再現可能な事実があると他の人も分かりやすいのですが…
まあとりあえずそういう事例があったと…

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

#9

ttb

総合スコア67

投稿2023/04/25 12:32

#8
確か10年くらい前なので詳細は失念してしまいましたが・・・
当時複数のクラスを作成し、作ったものはどんどん再利用すれば良いと思って、蜘蛛の糸のように依存関係を持ってヘッダをインクルードしてたような記憶です。
設計が悪かったといえばそれまでですが、自分としては、元になるクラスは同一なれど、インクルードガードしてるし大丈夫と思っていたんですが、cpp単位でコンパイルされて後でリンクされるというのを理解できていなかったため・・・
ヘッダに定義を書いたものを、複数のcppファイルからインクルードしていたんだと思います。
そして、リンクしてクラスを呼び出した時に、パイナリコードが2箇所以上に配置されており、それで実行時エラーになってたと推測です。

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

#10

dameo

総合スコア943

投稿2023/04/25 17:01

#9
すみませんが、そういうケースはコードによる実例を挙げて頂かないと、理解を得るのは難しいかと…。推測ではどうしようもないですね。

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

#11

fana

総合スコア11708

投稿2023/04/26 04:42

C++でライブラリとか作った経験がほぼ無いので,そっちの場合にはどうするのが良いのかよくわからないですが……

普通に(?:ライブラリでなく)書く場合だと,ヘッダに実装を書くかどうかは,そうすると余計な依存関係が増えるか否かで考えているように思います.
要は,「そのヘッダに書かれた実装Aが別の何かの定義Bを必要とするのだけど,Aを利用したいだけの側としては定義Bを知る必要が無いハズである場合」には,実装Aはヘッダに書かない……という感じ.
(そのためだけに pImpl とかいう面倒を持ち込んだりしますよね)


テンプレートに関しては,未知の型で使わせる必要があるならばヘッダに全部書くしかないという認識です.

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

#12

dameo

総合スコア943

投稿2023/04/26 05:16

#11
pimplですね。
https://learn.microsoft.com/ja-jp/cpp/cpp/pimpl-for-compile-time-encapsulation-modern-cpp

公開ライブラリの場合、設計の切れの良さと速度のどちらを重視するかは難しいですが、「インターフェイスと実装の分離」をすれば切れは良くなりますよね。ただ、低レイヤを担当することの多いC++だとインターフェイス切ったところをループでブン回すことも多く、ヘッダにして展開してもらい、最大限最適化の恩恵を受けたいと思ってしまうこともあります。難しいですよね。

テンプレートを多用する場合は本当に悩ましいです。ソースコードを共有できる相手であっても、チームが違えば担当も違うので、利用はするけど明示的なインスタンス化は任せられないし、ヘッダに展開するにはコードが重すぎたり、みたいなこともあるんですよね(規模が大きくなるとビルド時間の問題は多少考慮しないといけない場合がある)。設計が悪いと言えばそれまでなのですが、個々のケースはその場で考えるしかない感じです。

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

#13

fana

総合スコア11708

投稿2023/04/26 07:22

編集2023/04/26 07:26

(すっごい雑な解釈かと思いますが)静的ポリモフィズムというのは動的〃が実行時に支払っている処理コストをビルドに要する時間側に転嫁してる感じの物だと思うので,
「実行時の処理効率を極限まで追い求める必要が 本当に ある」のであれば,「ビルド所要時間の側にしわ寄せがいくのは仕方ないので^^」と開き直って「ヘッダオンリーライブラリ!」とか言って全てを利用側に押し付けるくらいの勢いが必要なのかもしれません.

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

#14

dameo

総合スコア943

投稿2023/04/26 07:29

#13
その辺りの基本は全員百も承知で作業しているのですが、全体のビルド時間が伸びることは作業効率的に好ましくないというだけですよ。開き直るとかそういう話ではなくてですね。。。

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

#15

fana

総合スコア11708

投稿2023/04/26 07:41

編集2023/04/26 08:55

すみません.基本1人で閉じたような作業しかしてない類のクソ雑魚なので,

全体のビルド時間が伸びる

の具体的イメージがなかなか持てないです.
あなたのテンプレートのコードが他者の作業と並行して変化していく場合の話なんでしょうか?
それとも,そのテンプレートの部分が利用側で何度も何度もコンパイル対象になる,という話なのでしょうか?


全然関係ないですが, extern template なんてのがあるんですね.
テンプレートの利用側で上手く使えばコンパイル時間が減るのかな?
(VS2019でちょっと試してみたら全然効かなくて「???」ってなりましたが… →inlineな感じに書いたメソッドには効かないとのことらしい

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

#16

dameo

総合スコア943

投稿2023/04/26 08:43

編集2023/04/26 09:03

#15
どちらもYesですね。何をするにもビルドは必要になるわけで、少しずつ開発の時間を奪います。
コーディングでもデバッグでもテストでも、何でもです。ある程度以上の規模になると、モジュールの数は百とかそれ以上になるわけで、フルビルドだって1時間とかそれ以上かかっちゃうわけです(マシン性能に強く依存しますが)。

百人百様なので卑下する必要はないのですが、ちょっと議題からは離れてきているので、何か具体的な検討対象でもない限り、この話(全体のビルド時間が伸びる)はここで終わりますね。


~ 追記内容について ~
#6 で書いたリンクに少しありますよ。
https://gcc.gnu.org/onlinedocs/gcc/Template-Instantiation.html (再掲)
明示的なインスタンス生成を強制するためにexternを付けるという話だと思ってました。実際に使ってみたことはありません。
なんというか明示的なインスタンス生成をライブラリ側でしたとしても、「実際何があんの?」が利用者側から見えないのでヘッダでexternするものかと思ってました。

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

最新の回答から1ヶ月経過したため この意見交換はクローズされました

意見をやりとりしたい話題がある場合は質問してみましょう!

質問する

関連した質問