まえがき
この記事は、 God Adapterという英語の記事からのオリジナル翻訳です。 C ++ロシアカンファレンスのビデオスピーチも見ることができます。
1要約
この記事では、必要な機能を追加して任意のオブジェクトを別のオブジェクトにラップできる特別なアダプターを紹介します。 適合オブジェクトは同じインターフェースを持っているため、使用に関しては完全に透過的です。 シンプルだが強力で興味深い例を使用して、共通の概念を順次紹介します。
2はじめに
警告 この記事で言及されているほとんどすべてのメソッドには、ダーティハックとC ++言語の異常な使用が含まれています。 そのため、このような倒錯に寛容でない場合は、この記事を読まないでください。
ユニバーサルアダプターという用語は、オブジェクトに必要な動作を普遍的に追加する機能に由来します。
3問題の声明
むかしむかし、共有データへのアクセスを簡素化するスマートミューテックスの概念を導入しました。 アイデアは単純でした。ミューテックスをデータに関連付け、データへのアクセスごとに自動的にlock
とunlock
を呼び出します。 コードは次のとおりです。
struct Data { int get() const { return val_; } void set(int v) { val_ = v; } private: int val_ = 0; }; // SmartMutex<Data> d; // , d->set(4); // std::cout << d->get() << std::endl;
しかし、このアプローチにはいくつかの問題があります。
3.1ブロッキング時間
ロックは、現在の式の実行時間全体にわたって保持されます。 次の行を検討してください。
std::cout << d->get() << std::endl;
Unlockは、 std::cout
への出力を含む、式全体の完了後に呼び出されます。 これは不必要な時間の無駄であり、ロックを取得する際の待ち時間が大幅に増加します。
3.2相互ブロックの可能性
最初の問題の結果として、暗黙のロック機構と現在の式の実行中の長いロック時間による相互ロックの可能性があります。 次のコードスニペットを検討してください。
int sum(const SmartMutex<Data>& x, const SmartMutex<Data>& y) { return x->get() + y->get(); }
関数がデッドロックを含む可能性があることはまったく明らかではありません。 これは、インスタンスx
とy
異なるペアに対して->get()
メソッドを任意の順序で呼び出すことができるためです。
したがって、ロックにかかる時間の増加を避け、上記のデッドロックを許可しない方がよいでしょう。
4ソリューション
考え方は非常に単純です。呼び出し自体の中にプロキシオブジェクトの機能を実装する必要があります。 そして、オブジェクトとの対話を簡素化するには、 ->
をに置き換えます.
。
簡単に言えば、 Data
オブジェクトを別のオブジェクトに変換する必要があります。
using Lock = std::unique_lock<std::mutex>; struct DataLocked { int get() const { Lock _{mutex_}; return data_.get(); } void set(int v) { Lock _{mutex_}; data_.set(v); } private: mutable std::mutex mutex_; Data data_; };
この場合、メソッド内でミューテックスを取得および解放する操作を制御します。 これにより、前述の問題が回避されます。
しかし、スマートミューテックスの基本的な考え方はコードの追加を避けることであるため、このようなレコードは実装には不便です。 推奨される方法は、両方のアプローチを活用することです。つまり、コードを減らし、同時に問題を減らします。 したがって、このソリューションを一般化し、幅広いユースケースに配布する必要があります。
4.1汎用アダプター
mutex
を使用しない古いData
実装を、 mutex
を含む実装に何らかの形で適応させる必要があります。これは、 DataLocked
クラスに似ているはずです。 これを行うには、メソッド呼び出しをラップして、動作をさらに変換します。
template<typename T_base> struct DataAdapter : T_base { // set void set(int v) { T_base::call([v](Data& data) { data.set(v); }); } };
ここで、 data.set(v)
呼び出しを延期し、 T_base::call(lambda)
にT_base::call(lambda)
ます。 T_base
可能な実装はT_base
です。
struct MutexBase { protected: template<typename F> void call(F f) { Lock _{mutex_}; f(data_); } private: Data data_; std::mutex mutex_; };
ご覧のとおり、 DataLocked
クラスのモノリシック実装を、作成されたアダプターの基本クラスの1つとして、 DataAdapter<T_base>
とMutexBase
2つのクラスに分割しました。 しかし、実際の実装は非常に近い: Data::set(v)
呼び出し中にミューテックスを保持します。
4.2その他の一般化
実装をまとめましょう。 MutexBase
実装はData
に対してのみ機能しData
。 これを改善しましょう:
template<typename T_base, typename T_locker> struct BaseLocker : T_base { protected: template<typename F> auto call(F f) { using Lock = std::lock_guard<T_locker>; Lock _{lock_}; return f(static_cast<T_base&>(*this)); } private: T_locker lock_; };
ここでは、いくつかの一般化が使用されています。
- 特定のミューテックス実装を使用しません。
std::mutex
またはBasicLockable
のBasicLockable
を実装するオブジェクトを使用できます。 -
T_base
は、同じインターフェイスを持つオブジェクトのインスタンスです。 これは、Data
でも、たとえばDataLocked
などの既に適合したData
オブジェクトでもDataLocked
。
したがって、以下を決定できます。
using DataLocked = DataAdapter<BaseLocker<Data, std::mutex>>;
4.3より一般化が必要
汎化の使用を止めることは不可能です。 入力パラメータを変換したい場合があります。 これを行うには、アダプターを変更します。
template<typename T_base> struct DataAdapter : T_base { void set(int v) { T_base::call([](Data& data, int v) { data.set(v); }, v); } };
また、 BaseLocker
実装BaseLocker
変換されます。
template<typename T_base, typename T_locker> struct BaseLocker : T_base { protected: template<typename F, typename... V> auto call(F f, V&&... v) { using Lock = std::lock_guard<T_locker>; Lock _{lock_}; return f(static_cast<T_base&>(*this), std::forward<V>(v)...); } private: T_locker lock_; };
4.4ユニバーサルアダプター
最後に、アダプターに関連付けられているテンプレートコードのサイズを小さくしましょう。 テンプレートが終了し、高度なイテレータマクロが機能します。
#define DECL_FN_ADAPTER(D_name) \ template<typename... V> \ auto D_name(V&&... v) \ { \ return T_base::call([](auto& t, auto&&... x) { \ return t.D_name(std::forward<decltype(x)>(x)...); \ }, std::forward<V>(v)...); \ }
DECL_FN_ADAPTER
使用DECL_FN_ADAPTER
と、 D_name
という名前のメソッドをラップできDECL_FN_ADAPTER
。 オブジェクトのすべてのメソッドをソートしてラップするだけです。
#define DECL_FN_ADAPTER_ITERATION(D_r, D_data, D_elem) \ DECL_FN_ADAPTER(D_elem) #define DECL_ADAPTER(D_type, ...) \ template<typename T_base> \ struct Adapter<D_type, T_base> : T_base \ { \ BOOST_PP_LIST_FOR_EACH(DECL_FN_ADAPTER_ITERATION, , \ BOOST_PP_TUPLE_TO_LIST((__VA_ARGS__))) \ };
これで、1行のみを使用してData
を調整できます。
DECL_ADAPTER(Data, get, set) // template<typename T, typename T_locker = std::mutex, typename T_base = T> using AdaptedLocked = Adapter<T, BaseLocker<T_base, T_locker>>; using DataLocked = AdaptedLocked<Data>;
そしてそれだけです!
5例
mutexベースのアダプターを確認しました。 他の興味深いアダプターを検討してください。
5.1リンク数アダプター
何らかの理由で、オブジェクトにshared_ptr
を使用する必要がある場合があります。 そして、この振る舞いをユーザーから隠す方が良いでしょう: operator->
を使う代わりに、私はoperator.
を使いたいoperator.
。 まあ、または少なくとも単純に.
。 実装は非常に簡単です。
template<typename T> struct BaseShared { protected: template<typename F, typename... V> auto call(F f, V&&... v) { return f(*shared_, std::forward<V>(v)...); } private: std::shared_ptr<T> shared_; }; // BaseShared template<typename T, typename T_base = T> using AdaptedShared = Adapter<T, BaseShared<T_base>>;
アプリケーション:
using DataRefCounted = AdaptedShared<Data>; DataRefCounted data; data.set(2);
5.2。 アダプターの組み合わせ。
時々、スレッド間でデータを調べることは素晴らしいアイデアです。 一般的なスキームは、 shared_ptr
とmutex
を組み合わせることです。 shared_ptr
はオブジェクトの寿命に関する問題を解決し、 mutex
使用して競合状態を防ぎます。
適合した各オブジェクトは元のオブジェクトと同じインターフェースを持っているため、いくつかのアダプターを簡単に組み合わせることができます。
template<typename T, typename T_locker = std::mutex, typename T_base = T> using AdaptedSharedLocked = AdaptedShared<T, AdaptedLocked<T, T_locker, T_base>>;
この用途では:
using DataRefCountedWithMutex = AdaptedSharedLocked<Data>; DataRefCountedWithMutex data; // // int v = data.get();
5.3非同期の例:コールバックから未来へ
未来へのステップ。 たとえば、次のインターフェイスがあります。
struct AsyncCb { void async(std::function<void(int)> cb); };
ただし、将来の非同期インターフェイスを使用したいと思います。
struct AsyncFuture { Future<int> async(); };
Future
インターフェイスは次のとおりです。
template<typename T> struct Future { struct Promise { Future future(); void put(const T& v); }; void then(std::function<void(const T&)>); };
一致するアダプター:
template<typename T_base, typename T_future> struct BaseCallback2Future : T_base { protected: template<typename F, typename... V> auto call(F f, V&&... v) { typename T_future::Promise promise; f(static_cast<T_base&>(*this), std::forward<V>(v)..., [promise](auto&& val) mutable { promise.put(std::move(val)); }); return promise.future(); } };
アプリケーション:
DECL_ADAPTER(AsyncCb, async) using AsyncFuture = AdaptedCallback<AsyncCb, Future<int>>; AsyncFuture af; af.async().then([](int v) { // });
5.4非同期の例:未来からコールバックまで
なぜなら それは過去に私たちを向け、それをホームタスクにしましょう。
5.5遅延アダプタ
開発者は怠け者です。 開発者との互換性のために任意のオブジェクトを適合させましょう。
これに関連して、怠とは、オブジェクトをオンデマンドで作成することを意味します。 次の例を考えてみましょう。
struct Obj { Obj(); void action(); }; Obj obj; // : Obj::Obj obj.action(); // : Obj::action obj.action(); // : Obj::action AdaptedLazy<Obj> obj; // ! obj.action(); // : Obj::Obj Obj::action obj.action(); // : Obj::action
すなわち アイデアは、オブジェクトの作成を最後まで遅らせることです。 ユーザーがオブジェクトの使用を決定した場合、オブジェクトを作成し、適切なメソッドを呼び出す必要があります。 基本クラスの実装は次のとおりです。
template<typename T> struct BaseLazy { template<typename... V> BaseLazy(V&&... v) { // state_ = [v...]() mutable { return T{std::move(v)...}; }; } protected: using Creator = std::function<T()>; template<typename F, typename... V> auto call(F f, V&&... v) { auto* t = boost::get<T>(&state_); if (t == nullptr) { // state_ = std::get<Creator>(state_)(); t = std::get<T>(&state_); } return f(*t, std::forward<V>(v)...); } private: // variant // : std::variant<Creator, T> state_; }; template<typename T, typename T_base = T> using AdaptedLazy = Adapter<T, BaseLazy<T_base>>;
これで、重い遅延オブジェクトを作成し、必要な場合にのみ初期化できます。 ただし、ユーザーには完全に透過的です。
6オーバーヘッド
アダプターのパフォーマンスを見てみましょう。 実際には、ラムダを使用し、それらを他のオブジェクトに転送します。 したがって、このようなアダプタのオーバーヘッドを知ることは非常に興味深いでしょう。
これを行うには、簡単な例を考えてみましょう。オブジェクト自体を使用してオブジェクト呼び出しをラップします。 同一のアダプターを作成し、そのような場合のオーバーヘッドを測定してください。 直接パフォーマンスを測定する代わりに、さまざまなコンパイラ用に生成されたアセンブラコードを見てみましょう。
まず、 on
メソッドでのみ動作するアダプターのシンプルなバージョンを作成しましょう。
#include <utility> template<typename T, typename T_base> struct Adapter : T_base { template<typename... V> auto on(V&&... v) { return T_base::call([](auto& t, auto&&... x) { return t.on(std::forward<decltype(x)>(x)...); }, std::forward<V>(v)...); } };
BaseValue
は、同じ型T
から直接メソッドを呼び出すための同一の基本クラスです。
template<typename T> struct BaseValue { protected: template<typename F, typename... V> auto call(F f, V&&... v) { return f(t, std::forward<V>(v)...); } private: T t; };
そして、ここにテストクラスがあります。
struct X { int on(int v) { return v + 1; } }; // int f1(int v) { X x; return x.on(v); } // int f2(int v) { Adapter<X, BaseValue<X>> x; return x.on(v); }
以下に、 オンラインコンパイラで得られた結果を示します 。
GCC 4.9.2
f1(int): leal 1(%rdi), %eax ret f2(int): leal 1(%rdi), %eax ret
Clang 3.5.1
f1(int): # @f1(int) leal 1(%rdi), %eax retq f2(int): # @f2(int) leal 1(%rdi), %eax retq
ご覧のとおり、 f1
とf2
違いはありません。つまり、コンパイラーは、ラムダオブジェクトの作成と受け渡しに関連するオーバーヘッドを最適化し、完全に排除できます。
7結論
この記事では、追加機能を使用してオブジェクトを別のオブジェクトに変換できるアダプターを紹介します。これにより、変換と呼び出しのオーバーヘッドなしでインターフェースが変更されません。 ベースアダプターのクラスは、任意のオブジェクトに適用できるユニバーサルトランスフォーマーです。 これらは、アダプターの機能を改善し、さらに拡張するために使用されます。 基底クラスのさまざまな組み合わせにより、非常に複雑なオブジェクトを簡単に作成できます。
この強力で楽しいテクニックは、以降の記事で使用および拡張されます。
便利なリンク
[1] github.com/gridem/GodAdapter
[2] bitbucket.org/gridem/godadapter
[3] ブログ:God Adapter
[4] C ++ロシアレポート:ユニバーサルアダプター
[5] Video C ++ Russia:ユニバーサルアダプター
[6] Habrahabr:便利なC ++マルチスレッドイディオム
[7] オンラインゴッドボルトコンパイラ