web-dev-qa-db-ja.com

大きなテンプレートの実装を処理するC ++推奨の方法

通常、C++クラスを宣言するときは、ヘッダーファイルに宣言のみを記述し、ソースファイルに実装を記述することをお勧めします。ただし、このデザインモデルはテンプレートクラスでは機能しないようです。

オンラインで見ると、テンプレートクラスを管理する最良の方法について2つの意見があるようです。

1.宣言とヘッダーの実装全体。

これはかなり簡単ですが、私の意見では、テンプレートが大きくなると、コードファイルを保守および編集することが困難になります。

2.最後にインクルードするテンプレートインクルードファイル(.tpp)に実装を記述します。

これは私にとってはより良い解決策のようですが、広く適用されているようには見えません。このアプローチが劣っている理由はありますか?

多くの場合、コードのスタイルは個人の好みやレガシースタイルによって決定されます。私は新しいプロジェクト(古いCプロジェクトをC++に移植)を始めており、OO設計に比較的慣れていないので、最初からベストプラクティスに従いたいと思っています。

10
fhorrobin

テンプレート化された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:

  • インターフェイスとメソッドの実装が混在しています。これは「単なる」読みやすさの問題です。これは通常の.h/.cppアプローチとは異なるため、これを保守できないと考える人もいます。ただし、これはC#やJavaなどの他の言語では問題ないことに注意してください。
  • 再構築の影響が大きい: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.cppqux.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:

  • 再構築の影響が大きい((1)と同じ)。

このソリューションは、.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:

  • .h/.cppの分離と同様に、再構築の影響を軽減します。
  • インターフェースとメソッドの定義は分離されています。

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.tppbar.cppが追加されていることです。これは不便であり、メソッドを呼び出すかどうかに応じて、クラスの2番目のインクルードを追加することは非常に醜いようです。ただし、再構築の影響は軽減されます。bar.cppの実装を変更した場合、Foo::fのみを再コンパイルする必要があります。ファイルqux.cppを再コンパイルする必要はありません。

概要:

ライブラリを実装する場合、通常、再構築の影響を気にする必要はありません。ライブラリのユーザーはリリースを取得してそれを使用し、ライブラリの実装はユーザーの日常の作業で変更されません。このような場合、ライブラリはアプローチ(1)または(2)そして、どれを選ぶかは単に好みの問題です。

ただし、アプリケーションで作業している場合、または会社の内部ライブラリで作業している場合、コードは頻繁に変更されます。したがって、再構築の影響に注意する必要があります。アプローチを選択する(3)は、開発者に追加のインクルードを受け入れさせる場合に適したオプションです。

6
pschill

.tppのアイデア(これまで使用したことがない)と同様に、ほとんどのインライン機能を-inl.hppファイルに入れます。このファイルは、通常の.hppファイルの最後に含まれています。

他の人が示すように、これにより、インライン実装の煩雑さ(テンプレートなど)を別のファイルに移動することで、インターフェースが読みやすくなります。一部のインターフェイスインラインを許可しますが、それらを小さな、通常は1行の関数に制限しようとします。

2
Bill Door

2番目のバリアントのプロコインの1つは、ヘッダーが整然としていることです。

欠点は、インラインIDEエラーチェックがあり、デバッガバインディングが台無しになっている可能性があります。

1

実装を別のファイルに入れ、ヘッダーファイルにはドキュメントと宣言のみを含めるというアプローチを非常に好みます。

おそらく、このアプローチが実際に多く使用されているのを見たことがないのは、正しい場所を見ていなかったからです;-)

または、おそらくソフトウェアの開発に少し余分な労力がかかるためです。しかし、クラスライブラリの場合、その努力は十分に価値があります(IMHO)。はるかに使いやすく、読みやすいライブラリで十分です。

このライブラリを例にとります: https://github.com/SophistSolutions/Stroika/

ライブラリ全体はこのアプローチで記述されており、コードに目を通すと、どれだけうまく機能するかがわかります。

ヘッダーファイルは実装ファイルとほぼ同じ長さですが、宣言とドキュメンテーションしかありません。

Stroikaの可読性を、お気に入りのstd c ++実装(gccまたはlibc ++またはmsvc)の可読性と比較してください。これらはすべてインラインヘッダー実装アプローチを使用しており、非常によく書かれていますが、IMHOは読み取り可能な実装ではありません。

0
Lewis Pringle