C++20 によるより強力なラムダ

C++20 のおかげで、ラムダはより強力になりました。さまざまなラムダの改善のうち、ラムダのテンプレート パラメータは私のお気に入りです。

C++20 テンプレート パラメーターを使用したラムダのサポート。デフォルトで構築でき、コピー代入をサポートします。状態がなく、未評価のコンテキストで使用できる場合。さらに、this ポインターを暗黙的にコピーしたことを検出します。これは、ラムダによる未定義の動作の重大な原因がなくなったことを意味します。

ラムダのテンプレート パラメータから始めましょう。

ラムダのテンプレート パラメータ

確かに、型付きラムダ、ジェネリック ラムダ、およびテンプレート ラムダ (ラムダのテンプレート パラメーター) の違いは微妙です。

4 つのラムダ バリエーション

次のプログラムは、実装にラムダを使用する add 関数の 4 つのバリエーションを示しています。

// templateLambda.cpp

#include <iostream>
#include <string>
#include <vector>

auto sumInt = [](int fir, int sec) { return fir + sec; }; // only to int convertible types (C++11)
auto sumGen = [](auto fir, auto sec) { return fir + sec; }; // arbitrary types (C++14)
auto sumDec = [](auto fir, decltype(fir) sec) { return fir + sec; }; // arbitrary, but convertible types (C++14)
auto sumTem = []<typename T>(T fir, T sec) { return fir + sec; }; // arbitrary, but identical types (C++20)

int main() {
 
 std::cout << std::endl;
 // (1)
 std::cout << "sumInt(2000, 11): " << sumInt(2000, 11) << std::endl; 
 std::cout << "sumGen(2000, 11): " << sumGen(2000, 11) << std::endl;
 std::cout << "sumDec(2000, 11): " << sumDec(2000, 11) << std::endl;
 std::cout << "sumTem(2000, 11): " << sumTem(2000, 11) << std::endl;
 
 std::cout << std::endl;
 // (2)
 std::string hello = "Hello ";
 std::string world = "world"; 
 // std::cout << "sumInt(hello, world): " << sumInt(hello, world) << std::endl; ERROR
 std::cout << "sumGen(hello, world): " << sumGen(hello, world) << std::endl;
 std::cout << "sumDec(hello, world): " << sumDec(hello, world) << std::endl;
 std::cout << "sumTem(hello, world): " << sumTem(hello, world) << std::endl;
 
 
 std::cout << std::endl;
 // (3)
 std::cout << "sumInt(true, 2010): " << sumInt(true, 2010) << std::endl;
 std::cout << "sumGen(true, 2010): " << sumGen(true, 2010) << std::endl;
 std::cout << "sumDec(true, 2010): " << sumDec(true, 2010) << std::endl; 
 // std::cout << "sumTem(true, 2010): " << sumTem(true, 2010) << std::endl; ERROR
 
 std::cout << std::endl;
 
}

プログラムのおそらく驚くべき出力を示す前に、4 つのラムダを比較したいと思います。

  • sumInt
    • C++11
    • 型付きラムダ
    • 変換可能な int 型のみを受け入れます
  • sumGen
    • C++14
    • 汎用ラムダ
    • すべてのタイプを受け入れます
  • sumDec
    • C++14
    • 汎用ラムダ
    • 2 番目のタイプは最初のタイプに変換可能でなければなりません
  • sumTem
    • C++20
    • テンプレート ラムダ
    • 最初のタイプと 2 番目のタイプは同じでなければなりません

これは、異なる型のテンプレート引数に対して何を意味しますか?もちろん、各ラムダは int を受け入れ (1)、型指定されたラムダ sumInt は文字列を受け入れません (2)。

bool true と int 2010 を使用してラムダを呼び出すと、驚くかもしれません (3)。

  • true が int に変換された整数であるため、sumInt は 2011 を返します。
  • sumGen は 2011 を返します。これは、true が int に昇格された整数であるためです。 sumInt と sumGen には微妙な違いがありますが、これを数行で説明します。
  • sumDec は 2 を返します。なぜですか? 2 番目のパラメーター sec の型は、最初のパラメーター fir の型になります:(decltype(fir) sec) のおかげで、コンパイラーは fir の型を推測し、それを sec の型にします。したがって、2010 は true に変換されます。式 fir + sec では、fir は 1 に拡張された整数です。最終的に、結果は 2 です。
  • sumTem は無効です。

Compiler Explorer と GCC のおかげで、これがプログラムの出力です。

sumInt と sumGen には興味深い違いがあります。呼び出し側の sumInt の場合は真値の積分昇格が発生しますが、演算式 fir + sec の sumGen の場合は真値の積分昇格が発生します。プログラムの重要な部分はここにあります

auto sumInt = [](int fir, int sec) { return fir + sec; }; 
auto sumGen = [](auto fir, auto sec) { return fir + sec; }; 

int main() {
 
 sumInt(true, 2010);
 sumGen(true, 2010);
 
}

C++ Insights (プログラムへのリンク) でコード スニペットを使用すると、違いが示されます。コンパイラによって生成されたコードの重要な部分のみを示します。

class __lambda_1_15
{
 public: 
 inline /*constexpr */ int operator()(int fir, int sec) const
 {
 return fir + sec;
 }
 
};

__lambda_1_15 sumInt = __lambda_1_15{};
 

class __lambda_2_15
{
 public: 
 template<class type_parameter_0_0, class type_parameter_0_1>
 inline /*constexpr */ auto operator()(type_parameter_0_0 fir, type_parameter_0_1 sec) const
 {
 return fir + sec;
 }
 
 #ifdef INSIGHTS_USE_TEMPLATE
 template<>
 inline /*constexpr */ int operator()(bool fir, int sec) const
 {
 return static_cast<int>(fir) + sec; // (2)
 }
 #endif
 
};

__lambda_2_15 sumGen = __lambda_2_15{};
 

int main()
{
 sumInt.operator()(static_cast<int>(true), 2010); // (1)
 sumGen.operator()(true, 2010);
}

コンパイラがラムダから関数オブジェクトを生成することはご存知だと思います。ご存じないかもしれませんが、Andreas Fertig が私のブログで彼のツール C++ Insights に関するいくつかの投稿を書いています。 1 つの投稿はラムダに関するものです:C++ Insights の投稿。

コード スニペットを注意深く調べると、違いがわかります。 sumInt は呼び出し側で整数昇格を実行しますが (1)、sumGen は算術式で実行します (2)。

正直なところ、この例は私にとって非常に啓発的でした。テンプレート ラムダのより一般的な使用例は、ラムダでのコンテナーの使用です。

コンテナのテンプレート パラメータ

次のプログラムは、コンテナーを受け入れるラムダを示します。各ラムダはコンテナーのサイズを返します。

// templateLambdaVector.cpp

#include <concepts>
#include <deque>
#include <iostream>
#include <string>
#include <vector>

auto lambdaGeneric = [](const auto& container) { return container.size(); }; 
auto lambdaVector = []<typename T>(const std::vector<T>& vec) { return vec.size(); };
auto lambdaVectorIntegral = []<std::integral T>(const std::vector<T>& vec) { return vec.size(); };

int main() {

 
 std::cout << std::endl;
 
 std::deque deq{1, 2, 3}; // (1) 
 std::vector vecDouble{1.1, 2.2, 3.3, 4.4}; // (1)
 std::vector vecInt{1, 2, 3, 4, 5}; // (1)
 
 std::cout << "lambdaGeneric(deq): " << lambdaGeneric(deq) << std::endl;
 // std::cout << "lambdaVector(deq): " << lambdaVector(deq) << std::endl; ERROR
 // std::cout << "lambdaVectorIntegral(deq): " << lambdaVectorIntegral(deq) << std::endl; ERROR

 std::cout << std::endl;
 
 std::cout << "lambdaGeneric(vecDouble): " << lambdaGeneric(vecDouble) << std::endl;
 std::cout << "lambdaVector(vecDouble): " << lambdaVector(vecDouble) << std::endl;
 // std::cout << "lambdaVectorIntegral(vecDouble): " << lambdaVectorIntegral(vecDouble) << std::endl;
 
 std::cout << std::endl;
 
 std::cout << "lambdaGeneric(vecInt): " << lambdaGeneric(vecInt) << std::endl;
 std::cout << "lambdaVector(vecInt): " << lambdaVector(vecInt) << std::endl;
 std::cout << "lambdaVectorIntegral(vecInt): " << lambdaVectorIntegral(vecInt) << std::endl;
 
 std::cout << std::endl;
 
}

lambdaGeneric は、メンバー関数 size() を持つ任意のデータ型で呼び出すことができます。 lambdaVector はより具体的です:std::vector のみを受け入れます。 lambdaVectorIntegral は C++20 の概念 std::integral を使用します。したがって、int などの整数型を使用する std::vector のみを受け入れます。これを使用するには、ヘッダー を含める必要があります。小さなプログラムは一目瞭然だと思います.

プログラム templateLambdaVector.cpp には、おそらく見逃している機能が 1 つあります。 C++17 以降、コンパイラは引数からクラス テンプレートの型を推測できます (1)。したがって、冗長な std::vector myVec{1, 2, 3} の代わりに、単純に std::vector myVec{1, 2, 3} と書くことができます。

次は?

私の次の投稿は、C++20 での残りのラムダの改善についてです。