pimplの問題の1つは、それを使用するとパフォーマンスが低下することです(追加のメモリ割り当て、不連続なデータメンバー、追加の間接参照など)。pimplのすべての利点が得られないという犠牲を払ってこれらのパフォーマンスのペナルティを回避する、pimplイディオムのバリエーションを提案したいと思います。アイデアは、クラス自体にすべてのプライベートデータメンバーを残し、プライベートメソッドのみをpimplクラスに移動することです。基本的なpimplと比較した場合の利点は、メモリが連続している(追加の間接参照がない)ことです。pimplをまったく使用しない場合と比較した場合の利点は次のとおりです。
- プライベート関数を非表示にします。
- これらのすべての関数が内部リンケージを持ち、コンパイラーがより積極的に最適化できるように構造化できます。
したがって、私の考えは、pimplをクラス自体から継承させることです(私は少し奇妙に聞こえますが、我慢してください)。次のようになります。
Ahファイル:
class A
{
A();
void DoSomething();
protected: //All private stuff have to be protected now
int mData1;
int mData2;
//Not even a mention of a PImpl in the header file :)
};
A.cppファイル:
#define PCALL (static_cast<PImpl*>(this))
namespace //anonymous - guarantees internal linkage
{
struct PImpl : public A
{
static_assert(sizeof(PImpl) == sizeof(A),
"Adding data members to PImpl - not allowed!");
void DoSomething1();
void DoSomething2();
//No data members, just functions!
};
void PImpl::DoSomething1()
{
mData1 = bar(mData2); //No Problem: PImpl sees A's members as it's own
DoSomething2();
}
void PImpl::DoSomething2()
{
mData2 = baz();
}
}
A::A(){}
void A::DoSomething()
{
mData2 = foo();
PCALL->DoSomething1(); //No additional indirection, everything can be completely inlined
}
私が見る限り、これを使用することによるパフォーマンスのペナルティはまったくありませんが、pimplはありません。いくつかの可能なパフォーマンスの向上とよりクリーンなヘッダーファイルインターフェイス。これが標準のpimplと比較した場合の1つの欠点は、データメンバーを非表示にできないため、それらのデータメンバーを変更しても、ヘッダーファイルに依存するすべての再コンパイルがトリガーされることです。しかし、私がそれを見ると、メンバーにメモリ内で隣接させることの利点またはパフォーマンスの利点のいずれかを得る(またはこのハックを行う)-「試行3が嘆かわしい理由」)。もう1つの注意点は、Aがテンプレートクラスの場合、構文が煩わしいことです(ご存知のとおり、mData1を直接使用することはできません。これを実行する必要があります-> mData1、依存型のtypenameとおそらくテンプレートキーワードの使用を開始する必要があります)およびテンプレート化されたタイプなど)。さらにもう1つの注意点は、元のクラスではプライベートを使用できなくなり、保護されたメンバーしか使用できないため、単なる継承だけでなく、継承クラスからのアクセスも制限できないことです。私は試しましたが、この問題を回避できませんでした。たとえば、匿名の名前空間で実際のpimplクラスを定義できるようにフレンド宣言を広くすることを期待して、pimplをフレンドテンプレートクラスにしようとしましたが、それだけでは機能しません。データメンバーをプライベートに保ち、匿名の名前空間で定義された継承pimplクラスがそれらにアクセスできるようにする方法について誰かが考えている場合は、ぜひご覧になってください!それは私の主な予約をこれを使用することから排除します。
しかし、私はこれらの警告が私が提案するものの利益のために許容できると感じています。
この「機能のみのpimpl」イディオムへの参照をオンラインで探しましたが、何も見つかりませんでした。私は人々がこれについてどう思うか本当に興味があります。これに関する他の問題や、これを使用すべきでない理由はありますか?
更新:
私はこの提案が多かれ少なかれ私が何であるかを正確に達成しようとするが、標準を変更することによってそうすることを見つけました。私はその提案に完全に同意し、それが標準になることを願っています(私はそのプロセスについて何も知らないので、それがどのくらい起こりそうかについてはわかりません)。組み込みの言語メカニズムを使用してこれを実行できるようにしたいのですが。この提案は、私よりもはるかに上手に達成しようとしていることの利点についても説明しています。また、私の提案のようにカプセル化を破る問題もありません(プライベート->保護されています)。それでも、その提案が標準になるまで(それが発生した場合)、私が提案したことで、リストに挙げた警告に従ってこれらの利点を得ることができると思います。
UPDATE2:
答えの1つは、LTOをいくつかの利点(私が推測しているより積極的な最適化)を得るための可能な代替手段として言及しています。さまざまなコンパイラ最適化パスで何が起こっているのか正確にはわかりませんが、結果のコードには少し経験があります(私はgccを使用しています)。元のクラスにプライベートメソッドを配置するだけで、外部リンクが必要になります。
私はここで間違っているかもしれませんが、私が解釈する方法は、すべての呼び出しインスタンスがそのTU内に完全にインライン化されている場合でも、コンパイル時オプティマイザは関数を削除できないということです。何らかの理由で、リンクされたバイナリ全体のすべての呼び出しインスタンスがすべてインライン化されているように見えても、LTOでも関数定義の削除を拒否します。関数ポインタを使用して関数を何らかの方法で呼び出すかどうかリンカーが知らないためであると述べているいくつかの参照を見つけました(リンカーがそのメソッドのアドレスが取得されないことを理解できない理由はわかりませんが) )。
私の提案を使用して、それらのプライベートメソッドを匿名の名前空間内のpimplに配置する場合、これは当てはまりません。それらがインライン化された場合、関数は(-finline-functionsを含む-O3を使用して)オブジェクトファイルに表示されません。
オプティマイザは、関数をインライン展開するかどうかを決定するときに、コードサイズへの影響を考慮して、それを理解しています。したがって、私の提案を使用して、オプティマイザがこれらのプライベートメソッドをインライン化できるように、少し「安く」しています。
PCALL
は未定義の動作です。基になるオブジェクトが実際にtypeでない限り、をにキャストしA
てPImpl
使用することはできませんPImpl
。しかし、私が間違っていない限り、ユーザーはタイプのオブジェクトを作成するだけA
です。