通常、C++クラスを宣言するときは、ヘッダーファイルに宣言のみを記述し、ソースファイルに実装を記述することをお勧めします。ただし、このデザインモデルはテンプレートクラスでは機能しないようです。
オンラインで見ると、テンプレートクラスを管理する最良の方法について2つの意見があるようです。
これはかなり簡単ですが、私の意見では、テンプレートが大きくなると、コードファイルを保守および編集することが困難になります。
これは私にとってはより良い解決策のようですが、広く適用されているようには見えません。このアプローチが劣っている理由はありますか?
多くの場合、コードのスタイルは個人の好みやレガシースタイルによって決定されます。私は新しいプロジェクト(古いCプロジェクトをC++に移植)を始めており、OO設計に比較的慣れていないので、最初からベストプラクティスに従いたいと思っています。
テンプレート化されたC++クラスを記述する場合、通常は3つのオプションがあります。
(1)宣言と定義をヘッダーに配置します
// foo.h
#pragma once
template <typename T>
struct Foo
{
void f()
{
...
}
};
または
// foo.h
#pragma once
template <typename T>
struct Foo
{
void f();
};
template <typename T>
inline void Foo::f()
{
...
}
Pro:
Con:
Foo
をメンバーとして新しいクラスを宣言する場合は、foo.h
を含める必要があります。つまり、Foo::f
の実装を変更すると、ヘッダーファイルとソースファイルの両方に反映されます。再構築の影響を詳しく見てみましょう。テンプレート化されていないC++クラスの場合は、宣言を.hに、メソッド定義を.cppに配置します。このように、メソッドの実装が変更された場合、1つの.cppのみを再コンパイルする必要があります。 .hにすべてのコードが含まれている場合、これはテンプレートクラスでは異なります。次の例を見てください。
// bar.h
#pragma once
#include "foo.h"
struct Bar
{
void b();
Foo<int> foo;
};
// bar.cpp
#include "bar.h"
void Bar::b()
{
foo.f();
}
// qux.h
#pragma once
#include "bar.h"
struct Qux
{
void q();
Bar bar;
}
// qux.cpp
#include "qux.h"
void Qux::q()
{
bar.b();
}
ここでは、Foo::f
の使用はbar.cpp
内のみです。ただし、Foo::f
の実装を変更した場合は、bar.cpp
とqux.cpp
の両方を再コンパイルする必要があります。 Qux
のどの部分もFoo::f
を直接使用していない場合でも、Foo::f
の実装は両方のファイルに存在します。大規模なプロジェクトの場合、これはすぐに問題になる可能性があります。
(2)宣言を.hに、定義を.tppに入れ、それを.hに含めます
// foo.h
#pragma once
template <typename T>
struct Foo
{
void f();
};
#include "foo.tpp"
// foo.tpp
#pragma once // not necessary if foo.h is the only one that includes this file
template <typename T>
inline void Foo::f()
{
...
}
Pro:
Con:
このソリューションは、.h/.cppのように、宣言とメソッド定義を2つの別々のファイルに分けます。ただし、ヘッダーにはメソッド定義が直接含まれているため、このアプローチには(1)と同じ再構築の問題があります。
(3).hに宣言を置き、.tppに定義を入れますが、.hには.tppを含めません
// foo.h
#pragma once
template <typename T>
struct Foo
{
void f();
};
// foo.tpp
#pragma once
template <typename T>
void Foo::f()
{
...
}
Pro:
Con:
Foo
メンバーをクラスBar
に追加するときは、ヘッダーにfoo.h
を含める必要があります。 .cppでFoo::f
を呼び出す場合、またにfoo.tpp
を含める必要があります。Foo::f
を実際に使用する.cppファイルのみを再コンパイルする必要があるため、このアプローチは再構築の影響を軽減します。ただし、これには代償が伴います。これらすべてのファイルにはfoo.tpp
を含める必要があります。上記の例を取り上げ、新しいアプローチを使用します。
// bar.h
#pragma once
#include "foo.h"
struct Bar
{
void b();
Foo<int> foo;
};
// bar.cpp
#include "bar.h"
#include "foo.tpp"
void Bar::b()
{
foo.f();
}
// qux.h
#pragma once
#include "bar.h"
struct Qux
{
void q();
Bar bar;
}
// qux.cpp
#include "qux.h"
void Qux::q()
{
bar.b();
}
ご覧のとおり、唯一の違いは、foo.tpp
にbar.cpp
が追加されていることです。これは不便であり、メソッドを呼び出すかどうかに応じて、クラスの2番目のインクルードを追加することは非常に醜いようです。ただし、再構築の影響は軽減されます。bar.cpp
の実装を変更した場合、Foo::f
のみを再コンパイルする必要があります。ファイルqux.cpp
を再コンパイルする必要はありません。
概要:
ライブラリを実装する場合、通常、再構築の影響を気にする必要はありません。ライブラリのユーザーはリリースを取得してそれを使用し、ライブラリの実装はユーザーの日常の作業で変更されません。このような場合、ライブラリはアプローチ(1)または(2)そして、どれを選ぶかは単に好みの問題です。
ただし、アプリケーションで作業している場合、または会社の内部ライブラリで作業している場合、コードは頻繁に変更されます。したがって、再構築の影響に注意する必要があります。アプローチを選択する(3)は、開発者に追加のインクルードを受け入れさせる場合に適したオプションです。
.tpp
のアイデア(これまで使用したことがない)と同様に、ほとんどのインライン機能を-inl.hpp
ファイルに入れます。このファイルは、通常の.hpp
ファイルの最後に含まれています。
他の人が示すように、これにより、インライン実装の煩雑さ(テンプレートなど)を別のファイルに移動することで、インターフェースが読みやすくなります。一部のインターフェイスインラインを許可しますが、それらを小さな、通常は1行の関数に制限しようとします。
2番目のバリアントのプロコインの1つは、ヘッダーが整然としていることです。
欠点は、インラインIDEエラーチェックがあり、デバッガバインディングが台無しになっている可能性があります。
実装を別のファイルに入れ、ヘッダーファイルにはドキュメントと宣言のみを含めるというアプローチを非常に好みます。
おそらく、このアプローチが実際に多く使用されているのを見たことがないのは、正しい場所を見ていなかったからです;-)
または、おそらくソフトウェアの開発に少し余分な労力がかかるためです。しかし、クラスライブラリの場合、その努力は十分に価値があります(IMHO)。はるかに使いやすく、読みやすいライブラリで十分です。
このライブラリを例にとります: https://github.com/SophistSolutions/Stroika/
ライブラリ全体はこのアプローチで記述されており、コードに目を通すと、どれだけうまく機能するかがわかります。
ヘッダーファイルは実装ファイルとほぼ同じ長さですが、宣言とドキュメンテーションしかありません。
Stroikaの可読性を、お気に入りのstd c ++実装(gccまたはlibc ++またはmsvc)の可読性と比較してください。これらはすべてインラインヘッダー実装アプローチを使用しており、非常によく書かれていますが、IMHOは読み取り可能な実装ではありません。