0

クライアント コードから隠したい内部構造を持つライブラリに複数のクラスがあります。クライアントの観点からは、各クラスはライブラリ クラスからクエリされ、不透明なポインターとしてのみ使用されます。例は次のとおりです。

struct SomeSystem;
void doSomethingToSomeSystem(SomeSystem* system, Parameters params);
void doSomethingElseToSomeSystem(SomeSystem* system, Parameters params);

実装側では、 SomeSystem には、呼び出し元には表示されない複数のメンバーがあります。これはすべて問題ありませんが、不格好な使用構文はあまり好きではありません。

SomeSystem* system = lib->getSomeSystem();
doSomethingToSomeSystem(system, params);
doSomethingElseToSomeSystem(system, params);

別のアプローチはこれです:

struct SomeSystem;
namespace somesystem {
    void doSomething(SomeSystem* system, Parameters params);
    void doSomethingElse(SomeSystem* system, Parameters params);
}

使用法コード:

SomeSystem* system = lib->getSomeSystem();
somesystem::doSomething(system, params);
somesystem::doSomethingElse(system, params);

doSomethingと呼ばれるグローバル メソッドを使用doSomethingElseして、別の型でも が定義されている場合は、関数のオーバーロードに依存することもできますdoSomething。ただし、この場合、IDE で SomeSystem のすべての「メンバー」を見つけるのは困難です。

私は実際にメンバー関数を使用したくなる:

struct SomeSystem {
    void doSomething(Parameters params);
    void doSomethingElse(Parameters params);
};

使用法コード:

SomeSystem* system = lib->getSomeSystem();
system->doSomething(params);
system->doSomethingElse(params);

最後のスニペットは私には良さそうに見えますが、 SomeSystem はもはや不透明なポインターではなく、実際にメンバーを定義しています。私はこれを少し警戒しています。潜在的な問題の 1 つは、1 つの定義ルールです。ただし、クラスの「パブリック」定義と「プライベート」定義は、異なる翻訳単位からしか見えません。ここに隠された他の悪いことはありますか?クライアント コードがスタックまたは new を使用して SomeSystem をインスタンス化しようとすると、明らかにプログラムがクラッシュします。しかし、私はそれを喜んで受け入れます。おそらく、パブリック インターフェイスでプライベート コンストラクターを提供することで、これを回避できます。

もちろん、別のアプローチは、純粋仮想メソッドを使用して抽象クラスを定義することです。ただし、絶対に必要でない場合に備えて、このオーバーヘッドは避けたいと思います。

編集:

明確にするために、クライアントがクラスをインスタンス化しないため、実装が使用するものとは異なるクラスの定義 (一部のメンバーが欠落している) を含むパブリックヘッダーをクライアントに含めることが合法であるかどうか疑問に思っています。

公開ヘッダー:

struct SomeSystem {
    void doSomething(Parameters params);
    void doSomethingElse(Parameters params);
};

プライベート ヘッダー:

struct SomeSystem {
    Member member;
    void doSomething(Parameters params);
    void doSomethingElse(Parameters params);
};

プライベート ソース (プライベート ヘッダーを含む):

void SomeSystem::doSomething(Parameters params) {
    ...
}
void SomeSystem::doSomethingElse(Parameters params) {
    ...
}

これはテスト時に機能しますが、何らかの形で標準に違反しているかどうかはわかりません。2 つのヘッダーが同じ翻訳単位に含まれることはありません。

4

1 に答える 1

0

PIMPL イディオムはおそらくこの状況では理想的ですが、すべてのアクセスで追加の間接化になるため、それがあります。

構文糖衣の直後の場合の代替手段は、ADLを利用することかもしれません-少なくともシステム名を関数名から除外します:

// publicly shared header file
namespace one_system
{
  struct system;
  typedef system* system_handle;
  void do_something(system_handle );
};

// private implementation
namespace one_system
{
  struct system {};
  void do_something( system_handle ) { cout << "one"; }
};


int main() {
  auto handle = /* SOMETHING TO GET THIS SYSTEM */;
  do_something(handle); //do_something found by ADL
  return 0;
}

編集

私は今でも PIMPL が理想的だと思っています。また、すでにあるものと比較して、必ずしも割り当てや追加のオーバーヘッドを行う必要はありません。

system* と関数宣言がある場合 (例のように)、コンパイラは既に間接化を行う必要があります。その関数へのジャンプ (別の翻訳単位で定義されているため) と、関数内のシステムにアクセスするための間接参照 (ポインターとして使用されるため) が必要になります。

本当に必要なのは、次のようにクラスのインターフェースを定義することだけです。

// put this in a namespace or name it according to the system
class interface
{
    system_handle m_system;

    public:
    interface( system_handle s ) : m_system( s ) {}
    interface() = delete;

    void do_something();
};

別の翻訳単位では、システム上で操作を実行するために do_something() が定義されています。lib->GetSystem() はインターフェースのインスタンスを返すことができました。インターフェイスはパブリック関数のみで構成されているため、ヘッダー ファイルで完全に宣言できます。システムは依然として完全に非公開です (lib のユーザーはその内容を宣言するヘッダー ファイルを持たないという点で)。

さらに、インターフェイスはユーザーが簡単にコピーできます。ポインタがどこから来るかは気にしないので、ライブラリは必要に応じて静的アドレスを渡すことができます。

私が見ることができる1つの欠点は、メンバー変数にアクセサーが必要になることです(そして、すべてのメンバー変数はプライベートであり、とにかくパブリックまたは保護されたアクセサーを持つべきだと主張する人がたくさんいます)。

もう 1 つは、インターフェイスの *this が do_something に渡され、おそらく必要ないことです。これは、ヘッダー ファイルにも do_something を定義することで解決できます。

void do_something() { do_something_to_system( m_system ); }

do_something をインライン化できるため、コンパイラは *this out でも最適化できるはずです。コンパイラは m_system をレジスタにロードして do_something_to_system を呼び出すコードを簡単に挿入できます (これは、例で述べたものと似ています)。 .

于 2015-03-31T16:57:14.270 に答える