ポリモーフィックオブジェクトをSTLアルゴリズムに渡す方法

Effective C ++の最初の章で読むことができるように、C ++は基本的に4つの異なる部分の組み合わせです。


これら4つのサブ言語は、本質的に、単一のC ++言語と呼ばれるものを構成します。 それらはすべて1つの言語で結合されているため、これにより、対話する機会が得られます。 この相互作用により、興味深い状況が生じる場合があります。 今日は、そのうちの1つ、つまりオブジェクト指向モデルとSTLの相互作用について検討します。 さまざまな形式をとることができます。この記事では、多型機能オブジェクトのSTLアルゴリズムへの転送を検討します。 これら2つの世界は常に良好な接触状態にあるとは限りませんが、それらの間にかなり良い橋を架けることができます。

画像

多型機能オブジェクト-それは何ですか?


C ++の機能オブジェクトとは、演算子()を呼び出すことができるオブジェクトを意味します。 ラムダ関数またはファンクターにすることができます。 ポリモーフィズムは、プログラミング言語とコンテキストによって異なることを意味しますが、ここでは、継承と仮想メソッドを使用するクラスのポリモーフィックオブジェクトを呼び出します。 つまり、多態的な機能オブジェクトは次のようなものです。

struct Base { int operator()(int) const { method(); return 42; } virtual void method() const { std::cout << "Base class called.\n"; } }; 

この機能オブジェクトは何の役にも立ちませんが、そのメソッドの実装はメインタスクからその子孫をSTLアルゴリズムに渡すことから私たちの注意をそらすことはないので、さらに便利です。 そして、相続人は仮想メソッドをオーバーライドします:

 struct Derived : public Base { void method() const override { std::cout << "Derived class called.\n"; } }; 

次のように、簡単な方法で相続人をSTLアルゴリズムに渡してみましょう。

 void f(Base const& base) { std::vector<int> v = {1, 2, 3}; std::transform(begin(v), end(v), begin(v), base); } int main() { Derived d; f(d); } 

このコードは何を出力すると思いますか?

ここにある
呼び出される基本クラス。
呼び出される基本クラス。
呼び出される基本クラス。

変だよね? オーバーロードされた仮想メソッドを持つDerivedクラスのオブジェクトをアルゴリズムに渡しましたが、アルゴリズムは代わりに基本クラスメソッドを呼び出すことにしました。 何が起こったかを理解するために、std ::変換関数のプロトタイプを見てみましょう。

 template< typename InputIterator, typename OutputIterator, typename Function> OutputIt transform(InputIterator first, InputIterator last, OutputIterator out, Function f); 

最後のパラメーター(関数f)を注意深く見て、値で渡されることに注意してください。 同じEffective C ++本の第20章で説明されているように、ポリモーフィックオブジェクトは値で渡すと「カットオフ」されます:Base constへの参照がDerived型のオブジェクトを指す場合でも、baseのコピーを作成すると、Derived型のオブジェクトではなくBase型のオブジェクトが作成されます。

したがって、STLアルゴリズムのコピーではなく、ポリモーフィックオブジェクトへのリンクを渡す方法が必要です。

どうやってやるの?

オブジェクトを別のオブジェクトでラップしましょう


この考えは一般的に最初に来ます:「問題? オブジェクトを参照渡しする必要があり、STLアルゴリズムが値によるオブジェクトのみを受け入れる場合、必要なポリモーフィックオブジェクトへのリンクを格納する中間オブジェクトを作成できますが、これはオブジェクトは既に値で渡すことができます。

これを行う最も簡単な方法は、ラムダ関数を使用することです。

 std::transform(begin(v), end(v), begin(v), [&base](int n){ return base(n); } 

これで、コードは次を表示します。

Derived class called.
Derived class called.
Derived class called.


これは機能しますが、コードにラムダ関数を負荷します。ラムダ関数は非常に短いですが、コードの優雅さのためではなく、技術的な理由のために書かれています。
さらに、実際のコードでは、はるかに長く見える場合があります。

 std::transform(begin(v), end(v), begin(v), [&base](module::domain::component myObject){ return base(myObject); } 

機能的なパラダイムを松葉杖として使用する冗長コード。

コンパクトなソリューション:std :: refを使用


ポリモーフィックオブジェクトを値で渡す別の方法があり、std :: refを使用することになります

 std::transform(begin(v), end(v), begin(v), std::ref(base)); 

効果は、ラムダ関数と同じです。

Derived class called.
Derived class called.
Derived class called.


おそらく今、「なぜ?」という質問があります。 たとえば、私はそれを持っています。 まず、これはどのようにコンパイルされましたか? std :: refは、リンクをモデル化するstd :: reference_wrapper型のオブジェクトを返します(ただし、演​​算子=を使用して別のオブジェクトに再割り当てできます)。 std :: reference_wrapperはどのように機能オブジェクトの役割を果たすことができますか? 私はcppreference.comの std :: reference_wrapperのドキュメントを見て、これを見つけました:
std :: reference_wrapper ::演算子()

参照が保存されているCallableオブジェクトを呼び出します。 この関数は、保存された参照がCallableオブジェクトを指す場合にのみ使用できます。
つまり、これはstd :: reference_wrapperの特別な機能です。std:: refがタイプFの機能オブジェクトを受け入れる場合、返されるリンクのオブジェクトシミュレータも機能タイプであり、その演算子()はタイプFの演算子()を呼び出します。それが必要でした。

同じ結果がよりシンプルで簡潔なコードによって達成されるため、このソリューションはラムダ関数を使用するよりも優れているようです。 おそらく、この問題に対する他の解決策があります-コメントでそれらを見てうれしいです。

Source: https://habr.com/ru/post/J354198/


All Articles