この時点で、このライブラリは実験的であり、純粋な好奇心です。インターフェースの安定性や実装の品質は保証されていません。あなた自身のリスクで使用してください。
Dynoは、バニラC ++よりもランタイム多型の問題をうまく解決します。非邪魔に満たすことができるインターフェイスを定義する方法を提供し、多型オブジェクトを保存し、仮想メソッドに派遣するための完全にカスタマイズ可能な方法を提供します。継承、ヒープの割り当て、または価値のセマンティクスの快適な世界を去る必要はなく、バニラC ++を上回っている間にそうすることができます。
Dynoは、Rust特性オブジェクト、Goインターフェイス、Haskellタイプのクラス、仮想概念とも呼ばれるものの純粋なライブラリー実装です。ボンネットの下では、型消去と呼ばれるC ++手法を使用します。これはstd::any 、 std::function 、および他の多くの有用なタイプの背後にあるアイデアです。
# include < dyno.hpp >
# include < iostream >
using namespace dyno ::literals ;
// Define the interface of something that can be drawn
struct Drawable : decltype(dyno::requires_(
" draw " _s = dyno::method< void (std::ostream&) const >
)) { };
// Define how concrete types can fulfill that interface
template < typename T>
auto const dyno::default_concept_map<Drawable, T> = dyno::make_concept_map(
" draw " _s = [](T const & self, std::ostream& out) { self. draw (out); }
);
// Define an object that can hold anything that can be drawn.
struct drawable {
template < typename T>
drawable (T x) : poly_{x} { }
void draw (std::ostream& out) const
{ poly_. virtual_ ( " draw " _s)(out); }
private:
dyno::poly<Drawable> poly_;
};
struct Square {
void draw (std::ostream& out) const { out << " Square " ; }
};
struct Circle {
void draw (std::ostream& out) const { out << " Circle " ; }
};
void f (drawable const & d) {
d. draw (std::cout);
}
int main () {
f (Square{}); // prints Square
f (Circle{}); // prints Circle
}または、これがあまりにも多くのボイラープレートであることがわかっていて、マクロを使用して立つことができる場合、次のことは同等です。
# include < dyno.hpp >
# include < iostream >
// Define the interface of something that can be drawn
DYNO_INTERFACE (Drawable,
(draw, void (std::ostream&) const )
);
struct Square {
void draw (std::ostream& out) const { out << " Square " ; }
};
struct Circle {
void draw (std::ostream& out) const { out << " Circle " ; }
};
void f (Drawable const & d) {
d. draw (std::cout);
}
int main () {
f (Square{}); // prints Square
f (Circle{}); // prints Circle
}これはC ++ 17ライブラリです。古いコンパイラをサポートする努力は行われません(申し訳ありません)。ライブラリは、次のコンパイラで作業することが知られています。
| コンパイラ | バージョン |
|---|---|
| GCC | > = 7 |
| クラン | > = 4.0 |
| Apple Clang | > = 9.1 |
ライブラリは、boost.hanaとboost.callabletraitsに依存しています。ユニットテストはLibawfulに依存し、ベンチマークはGoogle Benchmark、Boost.typeerasure、mpark.variantに依存しますが、ライブラリを使用する必要はありません。ローカル開発のために、 dependencies/install.shスクリプトを使用して、すべての依存関係を自動的にインストールできます。
Dynoはヘッダーのみのライブラリであるため、SEを構築するものは何もありません。コンパイラのヘッダー検索パスにinclude/ディレクトリを追加するだけで(そして依存関係が満たされていることを確認してください)、行ってもいいです。ただし、構築できるユニットテスト、例、およびベンチマークがあります。
(cd dependencies && ./install.sh) # Install dependencies; will print a path to add to CMAKE_PREFIX_PATH
mkdir build
(cd build && cmake .. -DCMAKE_PREFIX_PATH= " ${PWD} /../dependencies/install " ) # Setup the build directory
cmake --build build --target examples # Build and run the examples
cmake --build build --target tests # Build and run the unit tests
cmake --build build --target check # Does both examples and tests
cmake --build build --target benchmarks # Build and run the benchmarks プログラミングでは、一般的なインターフェイスがありますが、異なる動的タイプを使用してオブジェクトを操作する必要が非常に頻繁に発生します。 C ++は継承でこれを解決します:
struct Drawable {
virtual void draw (std::ostream& out) const = 0;
};
struct Square : Drawable {
virtual void draw (std::ostream& out) const override final { ... }
};
struct Circle : Drawable {
virtual void draw (std::ostream& out) const override final { ... }
};
void f (Drawable const * drawable) {
drawable-> draw (std::cout);
}ただし、このアプローチにはいくつかの欠点があります。そうです
邪魔になりますSquare and Circle Drawableインターフェイスを満たすためには、どちらもDrawableベースクラスから継承する必要があります。これには、これらのクラスを変更するライセンスを持つ必要があります。これにより、継承は非常に依存できなくなります。たとえば、どのようにしてstd::vector<int> Drawableインターフェイスを満たしますか?あなたは単にできません。
値セマンティクスと互換性がありません
継承では、オブジェクト自体の代わりに多型のポインターまたはオブジェクトへの参照を渡す必要があります。これは、言語の残りと標準ライブラリで非常にひどく再生されます。たとえば、 Drawable sのベクトルをどのようにコピーしますか?仮想clone()メソッドを提供する必要がありますが、インターフェイスを台無しにしたばかりです。
動的ストレージとしっかりと結合しています
価値セマンティクスがないため、通常、これらの多型オブジェクトをヒープに割り当てることになります。これは恐ろしく非効率的であり、意味的に間違っています。なぜなら、動的ストレージの期間はまったく必要ない可能性があり、自動ストレージ期間(スタック上の)のオブジェクトで十分だったからです。
インラインを防ぎます
95%の時間で、多型ポインターまたは参照を使用して仮想メソッドを呼び出すことになります。それには3つの間接が必要です。1つはオブジェクト内のvtableにポインターをロードするため、もう1つはvtableの適切なエントリをロードするため、もう1つは関数ポインターへの間接コール用です。このすべてのジャンプは、コンパイラが優れたインランスの決定を行うことを困難にします。ただし、間接コールを除くこれらの間接的なすべてはすべて回避できることがわかります。
残念ながら、これはC ++が私たちのために行った選択であり、これらは動的な多型が必要なときに拘束されるルールです。それとも本当にですか?
Dynoは、上記の欠点がなく、より多くのグッズなしで、C ++のランタイム多型の問題を解決します。そうです:
邪魔にならない
インターフェイスは、そのタイプの変更を必要とせずにタイプで満たすことができます。ヘック、タイプは異なる方法で同じインターフェイスを満たすことさえできます! Dynoを使用すると、ばかげたクラスの階層にさよならにキスすることができます。
値セマンティクスに基づいて100%
多型オブジェクトは、その自然な価値セマンティクスを使用して、そのまま渡すことができます。あなたはあなたの多型オブジェクトをコピーする必要がありますか?確かに、コピーコンストラクターがあることを確認してください。あなたは彼らがコピーされないようにしたいですか?確かに、削除されたとマークします。 Dynoでは、愚かなclone()メソッドとAPIのポインターの増殖は過去のものです。
特定のストレージ戦略と組み合わされていません
多型オブジェクトの保存方法は、実際には実装の詳細であり、そのオブジェクトの使用方法を妨げるものではありません。 Dynoは、オブジェクトの保存方法を完全に制御できます。あなたはたくさんの小さな多型オブジェクトを持っていますか?確かに、それらをローカルバッファに保管して、割り当てを避けましょう。それとも、ヒープに物を保管するのは理にかなっていますか?確かに、先に進んでください。
可能な限り最高のパフォーマンスを実現するための柔軟なディスパッチメカニズム
ポインターをvtableに保存することは、動的ディスパッチを実行するためのさまざまな実装戦略の1つにすぎません。 Dynoは、動的なディスパッチがどのように発生するかを完全に制御でき、実際にはVtablesを倒すことができます。ホットループで呼び出される関数がある場合は、たとえばオブジェクトに直接保存して、Vtable間接をスキップできます。また、アプリケーション固有の知識を使用することも、コンパイラがいくつかの動的な呼び出しを最適化する必要はありません。ライブラリレベルの想像力学化。
まず、一般的なインターフェイスを定義して名前を付けることから始めます。 Dynoは、それを行うための単純なドメイン固有の言語を提供します。たとえば、描画できるタイプを説明する描画可能Drawableインターフェイスを定義しましょう。
# include < dyno.hpp >
using namespace dyno ::literals ;
struct Drawable : decltype(dyno::requires_(
" draw " _s = dyno::method< void (std::ostream&) const >
)) { };これはdrawと呼ばれるstd::ostreamがあるものを表すものとしてDrawable可能であると定義します。 Dynoは、これらのインターフェイスの動的概念を呼び出します。これは、タイプ(C ++概念など)で満たされる要件のセットを説明するためです。ただし、C ++の概念とは異なり、これらの動的概念はランタイムインターフェイスを生成するために使用されるため、動的という名前です。上記の定義は基本的に次のものと同等です。
struct Drawable {
virtual void draw (std::ostream&) const = 0;
};インターフェイスが定義されたら、次のステップは、このインターフェイスを満たすタイプを実際に作成することです。継承で、あなたは次のようなものを書くでしょう:
struct Square : Drawable {
virtual void draw (std::ostream& out) const override final {
out << " square " << std::endl;
}
};Dynoでは、多型は非侵入であり、代わりにコンセプトマップと呼ばれるものを介して提供されます(C ++ 0xコンセプトマップ後):
struct Square { /* ... */ };
template <>
auto const dyno::concept_map<Drawable, Square> = dyno::make_concept_map(
" draw " _s = [](Square const & square, std::ostream& out) {
out << " square " << std::endl;
}
);このコンストラクトは、
dyno::namespaceで定義されているconcept_mapという名前のC ++ 14変数テンプレートの専門化です。次に、その専門化をdyno::make_concept_map(...)で初期化します。
ラムダの最初のパラメーターはdraw上記の方法として宣言したときに暗示される暗黙の*thisパラメーターです。非会員関数を消去することも可能です(関連するセクションを参照)。
このコンセプトマップは、タイプのSquareがDrawable概念をどのように満たすかを定義します。ある意味では、タイプのSquare概念の実装にマッピングし、それが名称を動機付けます。タイプが概念の要件を満たすと、タイプモデル(またはモデル)がその概念であると言います。そのSquare Drawable概念のモデルであるため、 Square Drawableものとして使用したいと思います。従来の継承を使用すると、次のような基本クラスへのポインターを使用します。
void f (Drawable const * d) {
d-> draw (std::cout);
}
f ( new Square{}); Dynoでは、多型と価値のセマンティクスが互換性があり、多型のタイプが渡される方法を高度にカスタマイズできます。これを行うには、 Drawableものをすべて保持できるタイプを定義する必要があります。 Drawable*の代わりに、多型関数との間を通過するのは、そのタイプです。このラッパーの定義を支援するために、 Dynoはdyno::poly Containerを提供します。これは、特定の概念を満たす任意のオブジェクトを保持できます。ご覧のとおり、 dyno::polyは二重の役割があります。多型オブジェクトを保存し、メソッドの動的派遣を処理します。あなたがする必要があるのは、 dyno::polyの上に薄いラッパーを書くことだけで、それを正確に望ましいインターフェイスを与えます。
struct drawable {
template < typename T>
drawable (T x) : poly_{x} { }
void draw (std::ostream& out) const
{ poly_. virtual_ ( " draw " _s)(out); }
private:
dyno::poly<Drawable> poly_;
};注:技術的には、インターフェイスで
dyno::poly直接使用できます。ただし、dyno::polyよりも実際の方法でラッパーを使用する方がはるかに便利であるため、ラッパーを書くことをお勧めします。
これを分解しましょう。まず、 Drawable概念をモデル化するものの多型容器であるメンバーpoly_定義します。
dyno::poly<Drawable> poly_;次に、任意のタイプTからこの容器を構築できるコンストラクターを定義します。
template < typename T>
drawable (T x) : poly_{x} { }ここでの言われていない仮定は、 T実際にDrawable概念をモデル化するということです。確かに、Type Tのオブジェクトからdyno::polyを作成すると、 Dynoは、 Drawable可能で定義されたTおよびTがある場合は、概念マップを調べます。そのようなコンセプトマップがない場合、ライブラリは、サポートしていないタイプからdyno::polyを作成しようとしていることを報告し、プログラムはコンパイルされません。
最後に、上記の定義の最も奇妙で最も重要な部分は、 draw方法の部分です。
void draw (std::ostream& out) const
{ poly_. virtual_ ( " draw " _s)(out); }ここで起こるのは、 drawableオブジェクトで.drawが呼び出されると、実際にdyno::polyに保存されているオブジェクトの"draw"関数の実装に動的な派遣を実行し、それを呼び出します。さて、 Drawableものを受け入れる関数を作成するために、インターフェイスのポインターと所有権について心配する必要はもうありません。
void f (drawable d) {
d. draw (std::cout);
}
f (Square{});ちなみに、これがすべて愚かであり、テンプレートを使用しているはずだったと思っているなら、あなたは正しいです。ただし、ランタイムの多型が必要な場合は、次のことを考えてみましょう。
drawable get_drawable () {
if ( some_user_input ())
return Square{};
else
return Circle{};
}
f (get_drawable());厳密に言えば、 dyno::polyをラップする必要はありませんが、そうすることで、 Dynoと残りのコードの間に素晴らしい障壁があります。また、上記の定義でdyno::polyどのように実装されたかをほとんど無視しました。ただし、 dyno::polyパフォーマンスのニーズに合わせてカスタマイズできる多型オブジェクト向けの非常に強力なポリシーベースのコンテナです。 drawableラッパーを作成することで、コードの残りに影響を与えることなくパフォーマンスのためにdyno::polyが使用する実装戦略を簡単に微調整できます。
dyno::polyでカスタマイズできる最初の側面は、オブジェクトがコンテナ内に保存される方法です。デフォルトでは、継承ベースの多型と同じように、実際のオブジェクトへのポインターを保存するだけです。ただし、これは多くの場合、最も効率的な実装ではないため、 dyno::polyカスタマイズできるようにする理由です。そのためには、ストレージポリシーをdyno::polyに渡すだけです。たとえば、 drawableラッパーを定義して、ローカルバッファーに最大16バイトのオブジェクトを保存しようとしますが、オブジェクトが大きい場合はヒープに戻ります。
struct drawable {
template < typename T>
drawable (T x) : poly_{x} { }
void draw (std::ostream& out) const
{ poly_. virtual_ ( " draw " _s)(out); }
private:
dyno::poly<Drawable, dyno::sbo_storage< 16 >> poly_;
// ^^^^^^^^^^^^^^^^^^^^^ storage policy
};私たちの定義では、ポリシー以外は何も変わらないことに注意してください。それはダイノの非常に重要な教義の1つです。これらのポリシーは実装の詳細であり、コードの書き方を変更しないでください。上記の定義を使用すると、以前と同じようにdrawable sを作成できるようになり、 16バイトのfitsからdrawableオブジェクトが作成された場合、割り当ては発生しません。しかし、それが適合しない場合、 dyno::polyヒープに十分な大きいバッファーを割り当てます。
実際に割り当てをしたくないとしましょう。問題ありません。ポリシーをdyno::local_storage<16>に変更するだけです。大きすぎてローカルストレージに収まることができないオブジェクトからdrawableものを構築しようとすると、プログラムはコンパイルされません。割り当てを保存するだけでなく、従来の継承ベースのアプローチと比較した場合、多型オブジェクトにアクセスするたびにポインター間接を保存しています。特定のユースケースのためにこれらの(重要な)実装の詳細を微調整することにより、プログラムを古典的な継承よりもはるかに効率的にすることができます。
dyno::remote_storageやdyno::non_owning_storageなど、その他のストレージポリシーも提供されています。 dyno::remote_storageデフォルトのものであり、常にヒープアロークされたオブジェクトへのポインターを保存します。 dyno::non_owning_storageそのオブジェクトの生涯を心配することなく、すでに存在するオブジェクトへのポインターを保存します。これにより、オブジェクトに非所有の多型ビューを実装できます。これは非常に便利です。
カスタムストレージポリシーは非常に簡単に作成できます。詳細については、 <dyno/storage.hpp>を参照してください。
dyno::polyを導入したとき、2つの役割があると述べました。 1つ目は多型オブジェクトを保存することであり、2つ目は動的ディスパッチを実行することです。ストレージをカスタマイズできるように、動的ディスパッチの実行方法もポリシーを使用してカスタマイズすることができます。たとえば、 drawableラッパーを定義して、ポインターをvtableに保存する代わりに、代わりにvtableをdrawableオブジェクト自体に保存するようにしましょう。このようにして、仮想関数にアクセスするたびに1つの間接を回避します。
struct drawable {
template < typename T>
drawable (T x) : poly_{x} { }
void draw (std::ostream& out) const
{ poly_. virtual_ ( " draw " _s)(out); }
private:
using Storage = dyno::sbo_storage< 16 >; // storage policy
using VTable = dyno::vtable<dyno::local<dyno::everything>>; // vtable policy
dyno::poly<Drawable, Storage, VTable> poly_;
}; Vtableポリシー以外には、 drawableタイプの定義に変更する必要はないことに注意してください。さらに、必要に応じて、vtableポリシーから独立してストレージポリシーを変更することができます。上記では、すべての間接を保存しているにもかかわらず、 drawableオブジェクトを大きくすることで(局所的に保持する必要があるため)、それに対して支払いをしています。 Vtableに多くの機能がある場合、これは法外になる可能性があります。代わりに、ほとんどのVtableをリモートで保存する方が理にかなっていますが、私たちが頻繁に呼ぶ少数の関数のみをインラインにします。 Dynoは、セレクターを使用して非常に簡単に行うことができます。これは、ポリシーが適用される機能をカスタマイズするために使用できます。
struct drawable {
template < typename T>
drawable (T x) : poly_{x} { }
void draw (std::ostream& out) const
{ poly_. virtual_ ( " draw " _s)(out); }
private:
using Storage = dyno::sbo_storage< 16 >;
using VTable = dyno::vtable<
dyno::local<dyno::only<decltype( " draw " _s)>>,
dyno::remote<dyno::everything_else>
>;
dyno::poly<Drawable, Storage, VTable> poly_;
};この定義を考えると、Vtableは実際には2つに分割されます。最初の部分は、 drawableオブジェクトにローカルであり、 draw方法のみが含まれています。 2番目の部分は、残りのメソッドを保持する静的ストレージのvtableへのポインターです(たとえば、破壊者)。
Dynoは、2つのVtableポリシー、 dyno::local<>とdyno::remote<>を提供します。これらのポリシーは両方とも、セレクターを使用してカスタマイズする必要があります。ライブラリによってサポートされているセレクターはdyno::only<functions...> 、 dyno::everything_else dyno::everything dyno::except<...>
概念を定義する場合、概念に関連付けられた少なくとも一部の機能のデフォルトの定義を提供できることがよくあります。たとえば、デフォルトでは、 draw (もしあれば)という名前のメンバー関数を使用して、 Drawable概念の抽象"draw"メソッドを実装することはおそらく理にかなっています。このために、 dyno::default_concept_mapを使用できます。
template < typename T>
auto const dyno::default_concept_map<Drawable, T> = dyno::make_concept_map(
" draw " _s = []( auto const & self, std::ostream& out) { self. draw (out); }
);さて、いくつかのタイプT Drawable概念をどのように満たすかを調べようとするたびに、概念マップが定義されていない場合は、デフォルトのコンセプトマップに戻ります。たとえば、新しいタイプのCircleを作成できます。
struct Circle {
void draw (std::ostream& out) const {
out << " circle " << std::endl;
}
};
f (Circle{}); // prints "circle" Circle 、 Circleの概念マップを明示的に定義していない場合でも、自動的にDrawableモデルです。一方、そのようなコンセプトマップを定義する場合、デフォルトのマップよりも優先されます。
template <>
auto dyno::concept_map<Drawable, Circle> = dyno::make_concept_map(
" draw " _s = [](Circle const & circle, std::ostream& out) {
out << " triangle " << std::endl;
}
);
f (Circle{}); // prints "triangle"タイプの完全なファミリのコンセプトマップを一度に定義することが時々便利です。たとえば、 std::vector<T> Drawableなモデルを作成したい場合がありますが、 Tストリームに印刷できる場合にのみ。これは、この(そうではない)秘密のトリックを使用することで簡単に実現できます。
template < typename T>
auto const dyno::concept_map<Drawable, std::vector<T>, std:: void_t <decltype(
std::cout << std::declval<T>()
)>> = dyno::make_concept_map(
" draw " _s = [](std::vector<T> const & v, std::ostream& out) {
for ( auto const & x : v)
out << x << ' ' ;
}
);
f (std::vector< int >{ 1 , 2 , 3 }) // prints "1 2 3 "
std::vectorを変更する必要がない方法に注意してください。古典的な多型でこれを行うにはどうすればよいですか?回答:できません。
Dynoは、arbitrary意的な引数で派遣される非会員の機能と機能を消去することもできます(ただし、1つの引数のみ)。これを行うには、dyno :: dyno::method dyno::functionを使用して概念を定義し、 dyno::Tプレースホルダーを使用して、引数を消去されていることを示します。
// Define the interface of something that can be drawn
struct Drawable : decltype(dyno::requires_(
" draw " _s = dyno::function< void (dyno::T const &, std::ostream&)>
)) { };上記で使用されるdyno::T const&パラメーターは、関数が呼び出されているオブジェクトのタイプを表します。ただし、最初のパラメーターである必要はありません。
struct Drawable : decltype(dyno::requires_(
" draw " _s = dyno::function< void (std::ostream&, dyno::T const &)>
)) { };概念の履行は、概念がメソッドか関数を使用するかを変更するのではなく、関数実装のパラメーターが概念で宣言された関数のパラメーターと一致することを確認してください。
// Define how concrete types can fulfill that interface
template < typename T>
auto const dyno::default_concept_map<Drawable, T> = dyno::make_concept_map(
" draw " _s = [](std::ostream& out, T const & self) { self. draw (out); }
// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ matches the concept definition
);最後に、 dyno::polyでfunctionを呼び出すと、 Dynoがどのパラメーターを派遣したいかを推測できないため、すべてのパラメーターを明示的に渡す必要があります。コンセプトのdyno::Tプレースホルダーで宣言されたパラメーターはdyno::poly自体に合格する必要があります:
// Define an object that can hold anything that can be drawn.
struct drawable {
template < typename T>
drawable (T x) : poly_{x} { }
void draw (std::ostream& out) const
{ poly_. virtual_ ( " draw " _s)(out, poly_); }
// ^^^^^ passing the poly explicitly
private:
dyno::poly<Drawable> poly_;
};