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


10

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

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

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

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

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

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

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


1
参照してください。この9歳の記事 codeproject.com上を。方法3は、あなたが説明したものです。あなたが信じているほど特別ではないようです。
Doc Brown

...またはここで、同じアプローチ、2014年からの記事:codeofhonour.blogspot.com/2014/11/...
ドク・ブラウン

2
密接に関連:stackoverflow.com/q/1208028/179910を。Gnuは通常、「。tpp」ではなく「.tcc」拡張子を使用しますが、それ以外はほとんど同じです。
ジェリーコフィン2018

私は常に「ipp」を拡張子として使用しましたが、私が書いたコードでも同じことをたくさんしました。
Sebastian Redl

回答:


6

テンプレート化された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()
{
    ...
}

プロ:

  • 非常に便利な使用法(ヘッダーを含めるだけ)。

短所:

  • インターフェイスとメソッドの実装が混在しています。これは「単なる」読みやすさの問題です。これは通常の.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はinside bar.cppです。ただし、の実装を変更した場合Foo::f、との両方bar.cppqux.cpp再コンパイルする必要があります。のFoo::f一部がQux直接を使用していない場合でも、両方のファイルにliveが実装されています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()
{
    ...
}

プロ:

  • 非常に便利な使用法(ヘッダーを含めるだけ)。
  • インターフェイスとメソッドの定義は分離されています。

短所:

  • 再構築の影響が大きい((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()
{
    ...
}

プロ:

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

短所:

  • 不便な使い方:FooクラスBarにメンバーを追加するときfoo.hは、ヘッダーに含める必要があります。Foo::f.cpp を呼び出す場合は、そこに含める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)を選択することをお勧めします。


2

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

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


1

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

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


2番目には、テンプレートパラメータ宣言の冗長性も多く必要です。これは、特にsfinaeを使用するときに非常に冗長になる可能性があります。そしてOPとは対照的に、特に冗長なボイラープレートのために、2番目のコードはより多くのコードを読み取ることが難しいと感じています。
ソペル

0

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

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

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

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

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

ヘッダーファイルは、実装ファイルとほぼ同じ長さですが、宣言とドキュメントだけで埋められています。

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

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.