4

私は、C ++プロジェクトでヘッダー結合を減らす簡単な方法を探しています。これは、もちろん完全な型を必要とする(使いすぎた)クラス構成が主な原因です。例えば:

// header A
class A
{
  B b; // requires header B
};

インターフェイスとpimplも検討しましたが、どちらも手動で記述/サポートしたくない定型コードを暗示しています(またはこれを自動化する方法はありますか?)。

そこで、メンバーをポインタとのようなフォワードに置き換えることを考えましたclass B* pB;が、これにはオブジェクトの作成と削除の処理が必要です。わかりました。削除にはスマートポインタを使用できますが(auto_ptr作成時に完全な型が必要なため、次のように言いますshared_ptr<class B> pB;)、オブジェクトの作成を今すぐ行うにはどうすればよいですか?

Aのコンストラクターでオブジェクトを作成できますpB = new B;が、これも手動であり、さらに悪いことに、複数のコンストラクターが存在する可能性があります...したがって、これを自動的に行う方法を探しています。これは簡単に機能します。インスタンス化を気にすることなく、の定義に変更B b;するように。autoobjptr<class B> pB;ApB

これは新しいアイデアではないと確信しているので、一般的な解決策やディスカッションへの参照を教えていただけますか?

更新:A明確にするために、との間の依存関係を壊そうとはしていませんが、ヘッダーが含まれている場合はヘッダーがB含まれないようにします。実際には、はの実装で使用されるため、一般的な解決策はのインターフェイスまたはpimplを作成することですが、今のところもっと簡単なものを探しています。BABAA

UPDATE2:仮想デストラクタ(不完全なタイプを許可するため)と組み合わせると、ここで提案されているような怠惰なポインタがトリックを実行することに突然気付きました(ブーストなどでこれの標準実装はありません)。標準的な解決策がない理由がまだわからず、車輪の再発明をしたいと思っています...

UPDATE3:突然、Sergey Tachenovは非常に単純な解決策(受け入れられた答え)を提供しましたが、それが実際に機能する理由を理解するのに30分かかりました... A()コンストラクターを削除するか、ヘッダーファイルでインラインで定義すると、魔法はもう機能しません(コンパイルエラー)。明示的な非インラインコンストラクターを定義すると、メンバーの構築(暗黙的なものも含む)は、型Bが完全な同じコンパイル単位(A.cpp)内で行われると思います。一方、Aコンストラクターがインラインの場合、メンバーの作成は他のコンパイルユニット内で行われる必要があり、Bそこでは不完全であるため機能しません。まあ、これは論理的ですが、今私は興味があります-この動作はC ++標準によって定義されていますか?

UPDATE4:うまくいけば、最終的な更新。上記の質問に関する議論については、受け入れられた回答とコメントを参照してください。

4

5 に答える 5

3

最初は、この質問に興味をそそられました。それは本当に難しいことのように見え、テンプレート、依存関係、およびインクルードに関するすべてのコメントは理にかなっています。しかし、実際にこれを実装しようとすると、驚くほど簡単であることがわかりました。ですから、私がその質問を誤解したか、その質問には実際よりもはるかに見づらいという特別な性質があります。とにかく、これが私のコードです。

これは栄光のautoptr.hです:

#ifndef TESTPQ_AUTOPTR_H
#define TESTPQ_AUTOPTR_H

template<class T> class AutoPtr {
  private:
    T *p;
  public:
    AutoPtr() {p = new T();}
    ~AutoPtr() {delete p;}
    T *operator->() {return p;}
};

#endif // TESTPQ_AUTOPTR_H

とてもシンプルに見えて、実際に機能するのだろうかと思ったので、テストケースを作成しました。これが私のbhです:

#ifndef TESTPQ_B_H
#define TESTPQ_B_H

class B {
  public:
    B();
    ~B();
    void doSomething();
};

#endif // TESTPQ_B_H

そしてb.cpp:

#include <stdio.h>
#include "b.h"

B::B()
{
  printf("B::B()\n");
}

B::~B()
{
  printf("B::~B()\n");
}

void B::doSomething()
{
  printf("B does something!\n");
}

次に、これを実際に使用するAクラスについて説明します。これがああです:

#ifndef TESTPQ_A_H
#define TESTPQ_A_H

#include "autoptr.h"

class B;

class A {
  private:
    AutoPtr<B> b;
  public:
    A();
    ~A();
    void doB();
};

#endif // TESTPQ_A_H

そしてa.cpp:

#include <stdio.h>
#include "a.h"
#include "b.h"

A::A()
{
  printf("A::A()\n");
}

A::~A()
{
  printf("A::~A()\n");
}

void A::doB()
{
  b->doSomething();
}

わかりました。最後に、Aを使用しますが、「bh」を含まないmain.cpp:

#include "a.h"

int main()
{
  A a;
  a.doB();
}

これで、実際には単一のエラーや警告なしでコンパイルされ、機能します。

d:\alqualos\pr\testpq>g++ -c -W -Wall b.cpp
d:\alqualos\pr\testpq>g++ -c -W -Wall a.cpp
d:\alqualos\pr\testpq>g++ -c -W -Wall main.cpp
d:\alqualos\pr\testpq>g++ -o a a.o b.o main.o
d:\alqualos\pr\testpq>a
B::B()
A::A()
B does something!
A::~A()
B::~B()

それはあなたの問題を解決しますか、それとも私はまったく違うことをしていますか?

編集1:それは標準かどうか?

さて、それは正しいことのようですが、今では他の興味深い質問につながります。以下のコメントでの議論の結果は次のとおりです。

上記の例ではどうなりますか?ahファイルはbhファイルを必要としません。これは、実際には何も行わずb、宣言するだけであり、AutoPtrクラスのポインターは常に同じサイズであるため、そのサイズを認識しているためです。Bの定義を必要とするautoptr.hの唯一の部分はコンストラクタとデストラクタですが、これらはahで使用されないため、ahにbhを含める必要はありません。

しかし、なぜああ、Bのコンストラクターを使用しないのですか?Aのインスタンスを作成するたびに、Bのフィールドは初期化されませんか?その場合、コンパイラはAのインスタンス化のたびにこのコードをインライン化しようとしますが、失敗します。B::B()上記の例では、呼び出しはa.cppユニットのコンパイル済みコンストラクターの先頭に置かれているように見えますA::A()が、標準ではそれが必要ですか?

最初は、インスタントが作成されるたびにコンパイラがフィールド初期化コードをインライン化するのを妨げるものは何もないように思われるので、A a;この擬似コードに変わります(もちろん実際のC ++ではありません)。

A a;
a.b->B();
a.A();

そのようなコンパイラは、標準に従って存在できますか?答えはノーです、彼らはできませんでした、そして標準はそれとは何の関係もありません。コンパイラが「main.cpp」ユニットをコンパイルするとき、A :: A()コンストラクタが何をするのかわかりません。の特別なコンストラクターを呼び出す可能性があるため、異なるコンストラクターで2回初期化さbれる前に、デフォルトのコンストラクターをインライン化します。bまた、定義されている「a.cpp」ユニットA::A()は個別にコンパイルされるため、コンパイラはそれをチェックする方法がありません。

さて、スマートコンパイラがBの定義を調べたい場合、デフォルトのコンストラクタ以外にコンストラクタがない場合はB::B()、コンストラクタに呼び出しを行わずA::A()、代わりにが呼び出されるたびにインライン化するとどうなるでしょうかA::A()。コンパイラには、Bに現在他のコンストラクタがない場合でも、将来コンストラクタがないことを保証する方法がないため、これも発生しません。これをBクラス定義のbhに追加するとします。

B(int b);

次に、その定義をb.cppに入れ、それに応じてa.cppを変更します。

A::A():
  b(17) // magic number
{
  printf("A::A()\n");
}

これで、a.cppとb.cppを再コンパイルすると、main.cppを再コンパイルしなくても、期待どおりに機能します。これはバイナリ互換性と呼ばれ、コンパイラはそれを破るべきではありません。ただし、B::B()呼び出しをインライン化すると、2つのBコンストラクターを呼び出すmain.cppになります。ただし、コンストラクターと非仮想メソッドを追加してもバイナリ互換性が損なわれることはないため、適切なコンパイラーでそれを行うことは許可されません。

このようなコンパイラが存在しない最後の理由は、実際には意味がないためです。メンバーの初期化がインライン化されている場合でも、コードサイズが増えるだけで、メソッド呼び出しが1つあるため、パフォーマンスはまったく向上しA::A()ません。このメソッドですべての作業を1か所で実行しないようにしましょう。

編集2:さて、Aのインラインおよび自動生成されたコンストラクターはどうですか?

A:A()発生する別の質問は、ahとa.cppの両方から削除するとどうなるかということです。何が起こるかです:

d:\alqualos\pr\testpq>g++ -c -W -Wall a.cpp
d:\alqualos\pr\testpq>g++ -c -W -Wall main.cpp
In file included from a.h:4:0,
                 from main.cpp:1:
autoptr.h: In constructor 'AutoPtr<T>::AutoPtr() [with T = B]':
a.h:8:9:   instantiated from here
autoptr.h:8:16: error: invalid use of incomplete type 'struct B'
a.h:6:7: error: forward declaration of 'struct B'
autoptr.h: In destructor 'AutoPtr<T>::~AutoPtr() [with T = B]':
a.h:8:9:   instantiated from here
autoptr.h:9:17: warning: possible problem detected in invocation of delete 
operator:
autoptr.h:9:17: warning: invalid use of incomplete type 'struct B'
a.h:6:7: warning: forward declaration of 'struct B'
autoptr.h:9:17: note: neither the destructor nor the class-specific operator 
delete will be called, even if they are declared when the class is defined.

関連する唯一のエラーメッセージは、「不完全な型'structB'の無効な使用」です。基本的には、main.cppにbhを含める必要があることを意味しますが、なぜですか?自動生成されたコンストラクターは、インスタンス化するときにインライン化されるためですamain.cppで。わかりましたが、これは常に発生する必要がありますか、それともコンパイラに依存しますか?答えは、コンパイラに依存することはできないということです。自動生成されたコンストラクターを非インラインにするコンパイラーはありません。その理由は、コードをどこに置くかがわからないためです。プログラマーの観点からは、答えは明らかです。コンストラクターは、クラスの他のすべてのメソッドが定義されているユニットに配置する必要がありますが、コンパイラーは、どのユニットがそれであるかを認識していません。さらに、クラスメソッドは複数のユニットに分散される可能性があり、場合によってはそれが理にかなっていることもあります(クラスの一部が何らかのツールによって自動生成される場合など)。

そしてもちろん、A::A()inlineキーワードを使用するか、その定義をAクラス宣言内に配置することによって明示的にインライン化すると、同じコンパイルエラーが発生し、おそらく少しわかりにくいものになります。

結論

自動インスタンス化されたポインタに上記の手法を採用することはまったく問題ないようです。私が確信していない唯一のことは、AutoPtr<B> b;ah内のものがどのコンパイラでも機能するということです。つまり、ポインターと参照を宣言するときに前方に削除されたクラスを使用できますが、テンプレートのインスタンス化パラメーターとして使用することは常に正しいですか?それは悪いことではないと思いますが、コンパイラーはそうではないと考えるかもしれません。グーグルもそれに関して有用な結果をもたらさなかった。

于 2010-12-15T18:21:35.070 に答える
1

これは、実装されているのと同じ方法で実装できると確信していunique_ptrます。違いは、allocated_unique_ptrコンストラクターがデフォルトでBオブジェクトを割り当てることです。

ただし、Bオブジェクトの自動構築が必要な場合は、デフォルトのコンストラクターでインスタンス化されること注意してください。

于 2010-12-15T15:41:03.003 に答える
0

pimpl_ptr<T>含まれているTを自動的に新規作成、削除、およびコピーする自動を作成できます。

于 2010-12-15T15:36:37.620 に答える
0

さて、あなたは自分で最良の解決策を与え、ポインターを使用newし、コンストラクターでそれらを使用します...コンストラクターが複数ある場合は、そこでそのコードを繰り返します。あなたはあなたのためにそれをする基本クラスを作ることができます、しかしそれは実装を神秘的にするだけでしょう...

のテンプレートについて考えたことはありclass Bますか?これにより、ヘッダーの相互依存関係も解決できますが、コンパイル時間が長くなる可能性があります...これにより、これらを回避しようとしている理由がわかります#include。コンパイル時間を測定しましたか?困っていますか?これは問題ですか?

更新:テンプレート方法の例:

// A.h
template<class T>
class A
{
public:
    A(): p_t( new T ) {}
    virtual ~A() { delete p_t }
private:
    T* p_t;
};

繰り返しになりますが、これによってコンパイル時間が長くなることはほとんどありません(B.hテンプレートインスタンスを作成するためにA<B>プルインする必要があります)。これにより、Aヘッダーとソースファイルのインクルードを削除できます。

于 2010-12-15T15:33:07.160 に答える
0

このアプローチの問題は、この方法でBのヘッダーファイルをインクルードすることを回避できますが、依存関係を実際に減らすことはできないということです。

依存関係を減らすためのより良い方法は、Bを別のヘッダーファイルで宣言された基本クラスから派生させ、Aでその基本クラスへのポインターを使用することです。Aのコンストラクターで正しい子孫(B)を手動で作成する必要があります。 、 もちろん。

また、AとBの間の依存関係が実際のものである可能性も非常に高く、その場合、Bのヘッダーファイルを人為的にインクルードすることを避けても、何も改善されていません。

于 2010-12-15T15:37:25.487 に答える