C++ コア ガイドライン:関数オブジェクトを操作として渡す

インターフェイスは、ユーザーと実装者の間の契約であるため、細心の注意を払って作成する必要があります。これは、操作を引数として渡す場合にも当てはまります。

今日はルール 40 について書いています。これは、関数オブジェクトが最新の C++ で非常に頻繁に使用されているためです。

T.40:関数オブジェクトを使用して演算をアルゴリズムに渡す

まず第一に、ルールがラムダ関数を明示的に言及せずに使用していることにイライラするかもしれません。この点については後で詳しく書きます。

文字列のベクトルをソートするには、さまざまな方法があります。

// functionObjects.cpp

#include <algorithm>
#include <functional>
#include <iostream>
#include <iterator>
#include <string>
#include <vector>

bool lessLength(const std::string& f, const std::string& s){ // (6) 
 return f.length() < s.length();
}

class GreaterLength{ // (7)
 public:
 bool operator()(const std::string& f, const std::string& s) const{
 return f.length() > s.length();
 }
};

int main(){

 std::vector<std::string> myStrVec = {"523345", "4336893456", "7234", 
 "564", "199", "433", "2435345"};

 std::cout << "\n"; 
 std::cout << "ascending with function object" << std::endl; 
 std::sort(myStrVec.begin(), myStrVec.end(), std::less<std::string>()); // (1)
 for (const auto& str: myStrVec) std::cout << str << " "; 
 std::cout << "\n\n";
 
 std::cout << "descending with function object" << std::endl; 
 std::sort(myStrVec.begin(), myStrVec.end(), std::greater<>()); // (2)
 for (const auto& str: myStrVec) std::cout << str << " "; 
 std::cout << "\n\n";

 std::cout << "ascending by length with function" << std::endl;
 std::sort(myStrVec.begin(), myStrVec.end(), lessLength); // (3)
 for (const auto& str: myStrVec) std::cout << str << " "; 
 std::cout << "\n\n";

 std::cout << "descending by length with function object" << std::endl;
 std::sort(myStrVec.begin(), myStrVec.end(), GreaterLength()); // (4)
 for (const auto& str: myStrVec) std::cout << str << " "; 
 std::cout << "\n\n";

 std::cout << "ascending by length with lambda function" << std::endl;
 std::sort(myStrVec.begin(), myStrVec.end(), // (5)
 [](const std::string& f, const std::string& s){ 
 return f.length() < s.length(); 
 });
 for (const auto& str: myStrVec) std::cout << str << " "; 
 std::cout << "\n\n";

}

このプログラムは、文字列の長さに基づいて、文字列のベクトルを辞書式に並べ替えます。 (1) と (2) の行で、標準テンプレート ライブラリの 2 つの関数オブジェクトを使用しました。関数オブジェクトは、呼び出し演算子 (operator ()) がオーバーロードされたクラスのインスタンスです。多くの場合、誤って呼ばれるファンクターがあります。行 (1) の呼び出し std::sort(myStrVec.begin(), myStrVec.end(), std::less()) と std::sort の違いに気付いていただければ幸いです。 (myStrVec.begin(), myStrVec.end(), std::greater<>()) (2 行目)。 2 番目の式 (std::greater<>()) は、述語の型を指定していませんが、C++14 以降で有効です。関数 (6)、関数オブジェクト (7)、ラムダ関数 (5) を使用して、(3)、(4)、(5) の行を並べ替えました。今回は、文字列の長さがソート基準でした。

完全を期すために、ここにプログラムの出力を示します。

ルールでは、「関数オブジェクトを使用して操作をアルゴリズムに渡す」必要があると述べています。

関数オブジェクトの利点

私の議論は、パフォーマンス、表現力、状態の 3 点に要約されます。ラムダ関数がフードの下の関数オブジェクトであることは、私の答えを非常に簡単にします。

パフォーマンス

オプティマイザーがローカルで推論できるほど、より多くの最適化が可能になります。関数オブジェクト (4) またはラムダ関数 (5) をその場で生成できます。これを別の翻訳単位で定義された関数と比較してください。信じられない場合は、コンパイラ エクスプローラを使用して、アセンブラの命令を比較してください。もちろん、最大限に最適化してコンパイルしてください。

表現力

「明示的は暗黙的よりも優れています」。この Python のメタルールは、C++ にも適用されます。これは、コードでその意図を明示的に表現する必要があることを意味します。もちろん、これは特にインライン (5) などのラムダ関数に当てはまります。これを、(3) 行で使用されている (6) 行の関数 lessLength と比較してください。同僚が関数に foo という名前を付けると想像してください。したがって、関数が何をすべきかわかりません。次の行のように、その使用法を文書化する必要があります。

// sorts the vector ascending, based on the length of its strings 
std::sort(myStrVec.begin(), myStrVec.end(), foo); 

さらに、同僚が正しい述語を書いていることを期待する必要があります。彼を信じられないなら、実装を検討する必要があります。関数の宣言があるだけなので、おそらくそれは不可能です。ラムダ関数を使えば、同僚にだまされることはありません。コードは真実です。もっと挑発的に言ってみましょう:コードは、ドキュメントを必要としないほど表現力豊かであるべきです。

状態

関数とは対照的に、関数オブジェクトは状態を持つことができます。コード例が私の主張を裏付けています。

// sumUp.cpp

#include <algorithm>
#include <iostream>
#include <vector>

class SumMe{
 int sum{0};
 public:
 SumMe() = default;

 void operator()(int x){
 sum += x;
 }

 int getSum(){
 return sum;
 }
};

int main(){

 std::vector<int> intVec{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}; 

 SumMe sumMe= std::for_each(intVec.begin(), intVec.end(), SumMe()); // (1)

 std::cout << "\n";
 std::cout << "Sum of intVec= " << sumMe.getSum() << std::endl; // (2)
 std::cout << "\n";

}

行 (1) の std::for_each 呼び出しは重要です。 std::for_each は、標準テンプレート ライブラリの特別なアルゴリズムです。これは、呼び出し可能オブジェクトを返すことができるためです。関数オブジェクト SumMe を使用して std::for_each を呼び出すため、関数呼び出しの結果を関数オブジェクトに直接格納できます。 (2) 行で、関数オブジェクトの状態であるすべての呼び出しの合計を求めます。

完成するだけです。ラムダ関数も宣言することができます。ラムダ関数を使用して値を累積できます。

// sumUpLambda.cpp

#include <algorithm>
#include <iostream>
#include <vector>

int main(){
 
 std::cout << std::endl;

 std::vector<int> intVec{1, 2, 3, 4, 5, 6, 7, 8, 9, 10};

 std::for_each(intVec.begin(), intVec.end(),
 [sum = 0](int i) mutable {
 sum += i; 
 std::cout << sum << std::endl;
 });
 
 std::cout << "\n";

}

さて、このラムダ関数は恐ろしく見えます。まず、変数 sum はラムダ関数の状態を表します。 C++14 では、いわゆるラムダの初期化キャプチャがサポートされています。 sum =0 は、ラムダ関数のスコープ内でのみ有効な int 型の変数を宣言して初期化します。 Lambda 関数はデフォルトの const ごとです。ミュータブルとして宣言することで、合計する数値を加算できます。

ラムダ関数は内部の関数オブジェクトであると述べました。 C++ Insight は、私の主張を簡単に証明します。

Lambda 関数は関数オブジェクトです

ラムダ関数は、その場でインスタンス化される関数オブジェクトのシンタックス シュガーにすぎません。 C++ インサイトは、コンパイラがラムダ関数に適用する変換を示します。

簡単に始めましょう。 C++ インサイトで次の小さなラムダ関数を実行すると

ツールは無糖のシンタックス シュガーを提供してくれます:

コンパイラは関数オブジェクト __lamda_2_16 を生成し (4 行目から 11 行目)、13 行目でインスタンス化し、14 行目でそれを使用します。それだけです!

次の例はもう少し複雑です。ここで、ラムダ関数 addTo は、コピーによってキャプチャされた変数 c に合計を追加します。

この場合、自動生成された関数オブジェクトはメンバー c とコンストラクターを取得します。これは C++ Insight のコードです。

次は?

これは、インターフェイスをテンプレート化するための最初のルールにすぎません。私の次の投稿は彼らの話の続きです。