Leistungsfähigere Lambdas mit C++20

Leistungsfähigere Lambdas mit C++20

Dank C++20 werden Lambdas leistungsfähiger. Von den verschiedenen Lambda-Verbesserungen sind Template-Parameter für Lambdas meine Favoriten.

Lambdas-Unterstützung mit C++20-Vorlagenparametern, kann standardmäßig erstellt werden und Kopierzuweisung unterstützen, wenn sie keinen Status haben und in nicht ausgewerteten Kontexten verwendet werden können. Außerdem erkennen sie, wenn Sie den this-Zeiger implizit kopieren. Dies bedeutet, dass eine wesentliche Ursache für undefiniertes Verhalten mit Lambdas weg ist.

Beginnen wir mit Vorlagenparametern für Lambdas.

Vorlagenparameter für Lambdas

Zugegeben, die Unterschiede zwischen typisierten Lambdas, generischen Lambdas und Template-Lambdas (Template-Parameter für Lambdas) sind subtil.

Vier Lambda-Variationen

Das folgende Programm präsentiert vier Variationen der Add-Funktion, die Lambdas für ihre Implementierung verwenden.

// 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;
 
}

Bevor ich die vermutlich erstaunliche Ausgabe des Programms zeige, möchte ich die vier Lambdas vergleichen.

  • sumInt
    • C++11
    • typisiertes Lambda
    • akzeptiert nur int konvertierbare Typen
  • sumGen
    • C++14
    • generisches Lambda
    • akzeptiert alle Typen
  • sumDec
    • C++14
    • generisches Lambda
    • der zweite Typ muss in den ersten Typ konvertierbar sein
  • sumTem
    • C++20
    • Vorlagen-Lambda
    • der erste Typ und der zweite Typ müssen identisch sein

Was bedeutet das für Template-Argumente mit unterschiedlichen Typen? Natürlich akzeptiert jedes Lambda ints (1), und das typisierte Lambda sumInt akzeptiert keine Zeichenfolgen (2).

Das Aufrufen der Lambdas mit dem bool true und dem int 2010 kann überraschend sein (3).

  • sumInt gibt 2011 zurück, da true zu int hochgestuft wird.
  • sumGen gibt 2011 zurück, weil true integral zu int heraufgestuft wird. Es gibt einen feinen Unterschied zwischen sumInt und sumGen, den ich in ein paar Zeilen vorstelle.
  • sumDec gibt 2 zurück. Warum? Der Typ des zweiten Parameters sec wird zum Typ des ersten Parameters fir:Dank (decltype(fir) sec) leitet der Compiler den Typ von fir ab und macht daraus den Typ von sec. Folglich wird 2010 in wahr umgewandelt. Im Ausdruck fir + sec wird fir ganzzahlig auf 1 hochgestuft. Schließlich ist das Ergebnis 2.
  • sumTem ist nicht gültig.

Dank Compiler Explorer und GCC ist hier die Ausgabe des Programms.

Es gibt einen interessanten Unterschied zwischen sumInt und sumGen. Die integrale Umwandlung des wahren Werts geschieht bei sumInt auf der Aufruferseite, aber die integrale Umwandlung des wahren Werts geschieht bei sumGen in den arithmetischen Ausdruck fir + sec. Hier noch einmal der wesentliche Teil des Programms

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);
 
}

Wenn ich das Code-Snippet in C++ Insights (Link zum Programm) verwende, zeigt es den Unterschied. Ich zeige nur den entscheidenden Teil des vom Compiler generierten Codes.

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);
}

Ich nehme an, Sie wissen, dass der Compiler aus einem Lambda ein Funktionsobjekt generiert. Falls Sie es nicht wissen, Andreas Fertig hat auf meinem Blog einige Beiträge zu seinem Tool C++ Insights geschrieben. Ein Beitrag handelt von Lambdas:C++ Insights posts.

Wenn Sie das Code-Snippet sorgfältig studieren, sehen Sie den Unterschied. sumInt führt die integrale Umwandlung auf der Aufrufseite durch (1), sumGen jedoch in den arithmetischen Ausdrücken (2).

Ehrlich gesagt war dieses Beispiel sehr aufschlussreich für mich und hoffentlich auch für Sie. Ein typischerer Anwendungsfall für Vorlagen-Lambdas ist die Verwendung von Containern in Lambdas.

Vorlagenparameter für Container

Das folgende Programm präsentiert Lambdas, die einen Container akzeptieren. Jedes Lambda gibt die Größe des Containers zurück.

// 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 kann mit jedem Datentyp aufgerufen werden, der eine Memberfunktion size() hat. lambdaVector ist spezifischer:Es akzeptiert nur einen std::vector. lambdaVectorIntegral verwendet das C++20-Konzept std::integral. Folglich akzeptiert es nur einen std::vector mit ganzzahligen Typen wie int. Um es zu verwenden, muss ich den Header einfügen. Ich gehe davon aus, dass das kleine Programm selbsterklärend ist.

Es gibt eine Funktion im Programm templateLambdaVector.cpp, die Sie wahrscheinlich übersehen haben. Seit C++17 kann der Compiler den Typ eines Klassen-Templates aus seinen Argumenten ableiten (1). Folglich können Sie statt des ausführlichen std::vector myVec{1, 2, 3} einfach std::vector myVec{1, 2, 3}.

schreiben

Was kommt als nächstes?

In meinem nächsten Beitrag geht es um die verbleibenden Lambda-Verbesserungen in C++20.