C ++の拡張メソッド

数日前、BjörnStraustrupは「 コール構文:xf(y)vs. f(x、y) 」と呼ばれるC ++標準化委員会に提案N4174を公​​開しました。 その本質の簡単な要約です:式f(x、y) (引数xおよびyを使用した関数fの呼び出し)と同等の式xf(y) (引数y を使用したメソッドfのオブジェクトxの呼び出しを宣言します。 つまり



xf(y)の意味:

  1. xf(y)の呼び出しを試してください。オブジェクトxのクラスにメソッドyが含まれ、引数yを取ることができる場合、このメソッドを使用します。
  2. 項目#1が失敗した場合、引数xおよびyを取ることができる関数fが存在するかどうかを確認します。 もしそうなら、それを使用します。
  3. どちらも見つからない場合、エラーが生成されます。


f(x、y)はまったく同じことを意味します。

  1. xf(y)の呼び出しを試してください。オブジェクトxのクラスにメソッドyが含まれ、引数yを取ることができる場合、このメソッドを使用します。
  2. 項目#1が失敗した場合、引数xおよびyを取ることができる関数fが存在するかどうかを確認します。 もしそうなら、それを使用します。
  3. どちらも見つからない場合、エラーが生成されます。


したがって、多くのC ++プログラマが夢見ていた拡張メソッドを作成する機会が得られます。 この提案は、C ++言語の進化において最も重要なものの1つであると考えています。



Cの拡張メソッド#



私たちが話していることをよりよく理解するために、C#での拡張メソッドの実装方法を思い出しましょう。



拡張メソッドを使用すると、元の型を変更したり、継承した型を作成したりすることなく、元の型を含むモジュールを再コンパイルすることなく、既存の型に機能を追加できます。 文字列クラスにその中の単語の数をカウントするメソッドを追加するとします。 これを行うには、次のようなWordCountメソッドを記述できます(簡単にするために、単語区切り文字は単なるスペース文字であると見なします)。



static class StringUtilities { public static int WordCount(this string text) { return text.Split(new char[] { ' ' }, StringSplitOptions.RemoveEmptyEntries).Length; } }
      
      







これで、次のように使用できます。



 var text = "This is an example"; var count = text.WordCount();
      
      





WordCount(テキスト)とtext.WordCount()の等価性は、文書N4174でStraustrupがまさに述べていることです。



C#の拡張メソッドにはいくつかの制限があることに注意してください。







C ++の拡張メソッド



誰かが尋ねるかもしれない質問は、「言語のxf(y)とf(x、y)の等価性の利点は何ですか?」です。 簡単な答えは、既存のコードを変更せずに拡張メソッドを定義して使用することを可能にします。



実際の例を見てみましょう。 C ++の標準コンテナは、特定のアイテムを見つけるためのfind()メソッドを提供します。 ただし、find()メソッドはイテレータを返すため、要素が見つかったかどうかを理解するには、equity end()を確認する必要があります。 同時に、多くの場合、要素自体を見つける必要はありませんが、コンテナに含まれているかどうかを確認する必要があります。 標準コンテナにはcontains()メソッドはありませんが、この関数を書くことができます。



 template<typename TKey, typename TValue> bool contains(std::map<TKey, TValue> const & c, TKey const key) { return c.find(key) != c.end(); }
      
      







そして、次のように呼び出します:



 auto m = std::map<int, char> {{1, 'a'}, {2, 'b'}, {3,'c'}}; if(contains(m, 1)) { std::cout << "key exists" << std::endl; }
      
      







しかし、実際にはオブジェクト指向プログラミングの世界では次のように書くのがいいでしょう。



 if(m.contains(1)) { }
      
      







xf(y)とf(x、y)が同等の場合-上記のコードは完全に有効(かつ美しい)です。



次に2番目の例を示します。 .NETでのLINQの演算子に類似したいくつかの演算子を定義するとします。 以下は、std :: vectorのこれらの演算子の一部の(簡略化された)実装の例です。



 template<typename T, typename UnaryPredicate> std::vector<T> where(std::vector<T> const & c, UnaryPredicate predicate) { std::vector<T> v; std::copy_if(std::begin(c), std::end(c), std::back_inserter(v), predicate); return v; } template <typename T, typename F, typename R = typename std::result_of<F(T)>::type> std::vector<R> select(std::vector<T> const & c, F s) { std::vector<R> v; std::transform(std::begin(c), std::end(c), std::back_inserter(v), s); return v; } template<typename T> T sum(std::vector<T> const & c) { return std::accumulate(std::begin(c), std::end(c), 0); }
      
      







これで、次のような「特定の範囲の偶数の平方を合計する」などの問題を解決できます。



 auto v = std::vector<int> {1,2,3,4,5,6,7,8,9}; auto t1 = where(v, [](int e){return e % 2 == 0; }); auto t2 = select(t1, [](int e){return e*e; }); auto s = sum(t2);
      
      







上記のコードは、次の呼び出しに渡すためだけに必要な多くの中間変数を作成するため、好きではありません。 それらを取り除くことができます:

 auto s = sum(select(where(v, [](int e){return e % 2 == 0; }), [](int e){return e*e; }));
      
      







しかし、私はこのコードがさらに好きではありません。 まず、読みにくいです(1行の操作が多すぎるため、他の書式設定でも役に立たない)。 次に、操作が実行される方法に関して、逆の順序で操作が表示されます。最初にsum呼び出しが表示され、次にselectが表示されます。 1つの関数の引数がどこで終わり、2番目の関数の引数がどこで始まるかを理解することもあまり便利ではありません。



ただし、言語標準がxf(y)とf(x、y)の等価性を決定する場合、次のコードを記述するのは非常に簡単です。

 auto v = std::vector<int> {1,2,3,4,5,6,7,8,9}; auto s = v.where([](int e){return e % 2 == 0; }) .select([](int e){return e*e; }) .sum();
      
      







本当に美しい? そう思う。



おわりに



これまでの文書N4174は、正式な標準というよりも理論的な可能性の研究のように見えます。 慎重に検討する必要がある多くの異なる側面があります。 興味がある場合は、ドキュメントを自分で読んでください。 それにもかかわらず、この機能は間違いなく有用に見えます。標準言語に移行する日が来ることを願っています。



All Articles