Mächtigere Lambda-Ausdrücke mit C++20
Dank dem C++20-Standard werden Lambda-Ausdrücke mächtiger. Von den vielen Verbesserungen rund um Lambda-Ausdrücke sind Template-Parameter mein Favorit.
Dank dem C++20-Standard werden Lambda-Ausdrücke mächtiger. Von den vielen Verbesserungen rund um Lambda-Ausdrücke sind Template-Parameter mein Favorit.
Lamba-Ausdrücke (Lambdas) unterstützen mit C++20 Template-Parameter, besitzen einen Default-Konstruktor und einen Copy-Zuweisungsoperator, wenn sie keinen Zustand besitzen und können in nicht evaluierten Kontexten verwendet werden. Zusätzlich stellen sie fest, wenn der this
-Zeiger implizit kopiert wird. Das heißt, dass eine häufige Ursache von undefinierten Verhalten mit Lambdas der Vergangenheit angehört.
Der Artikel beginnt mit Template-Parametern für Lambdas.
Template-Parameter für Lambdas
Zugegeben, die Unterschiede zwischen typisierten Lambdas, generischen Lambdas und Template Lambas (Template-Parameter für Lambdas) sind subtil.
Vier Lambda Variationen
Das folgende Programm verwendet vier Variationen der add
Funktion, die mit Lambdas implementiert ist.
// templateLambda.cpp
#include <iostream>
#include <string>
#include <vector>
// only to int convertible types (C++11):
auto sumInt = [](int fir, int sec) { return fir + sec; };
// arbitrary types (C++14):
auto sumGen = [](auto fir, auto sec) { return fir + sec; };
// arbitrary, but convertible types (C++14):
auto sumDec = [](auto fir, decltype(fir) sec) { return fir + sec; };
// arbitrary, but identical types (C++20):
auto sumTem = []<typename T>(T fir, T sec) { return fir + sec; };
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 wohl überraschende Ausgabe des Programms vorstelle, möchte ich die vier Lambdas kurz vergleichen.
sumInt
- C++11
- typisierte Lambda
- nimmt nur nach
int
konvertierbare Datentypen an
sumGen
- C++14
- generische Lambda
- nimmt alle Datentypen an
sumDec
- C++14
- generische Lambda
- der zweite Datentyp muss sich zum ersten Daten konvertieren lassen
sumTem
- C++20
- Template Lambda
- der erste und der zweite Datentyp müssen identisch sein
Was bedeutet, wenn die Template-Argumente verschiedene Datentypen besitzen? Klar, jedes Lambda nimmt int
an (1) und das typisierte Lambda sumInt
nimmt keinen std::string
an (2).
Der Aufruf der Lambdas mit dem bool
true
und dem int
2010 birgt
einiges Überraschungspotential (3).
-
sumInt
gibt2011
zurück, datrue
zuint
erweitert wird (integral promotion). sumGen
gibt2011
zurück, datrue
zuint
erweitert wird. Es gibt aber einen feinen Unterschied zwischensumInt
undsumGen
, dazu unten mehr.sumDec
gibt 2 zurück. Warum? Der Datentyp des zweiten Parameterssec
erhält den Datentyp des ersten Parameters fir.
Dankdecltype(fir) sec
ermittelt der Compiler den Datentyp vonfir
und wendet den gleichen Datentyp aufsec
an. Daher wird 2010 zutrue
. In dem Ausdruckfir +
sec
wirdfir
zur1
erweitert und somit ist das Ergebnis2
.sumTem
ist nicht gültig.
Dank dem Compiler Explorer [1] und GCC lässt sich das Programm ausführen.
Zwischen den Funktionen sumInt
und sumGen
besteht ein feiner Unterschied. Die Erweiterungen des true
Werts passiert im Falle der sumInt
Funktion beim Aufrufenden. Jedoch findet die Erweiterung des true Wertes bei der Funktion sumGen
in dem arithmetischen Ausdruck fir + sec
statt. Hier ist der entscheidende Teil des Programms nochmals.
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 den Code-Schnipsel in C++ Insights verwende [2], lässt sich der Unterschied genau studieren. Ich stelle in dem folgenden Code nur den entscheidenden Teil des vom Compiler erzeugten Codes dar.
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);
}
Vermutlich ist bekannt, dass der Compiler automatisch ein Funktionsobjekt aus einem Lambda-Ausdruck erzeugt. Falls nicht, möchte ich auf Andreas Fertigs Artikel zu seinem Werkzeug C++ Insights auf meinem Blog verweisen. Ein Artikel beschäftigt sich explizit mit Lambdas: C++ Insights Artikel [3].
Sorgfältiges Studieren des Codeschnipsels enthüllt den Unterschied: sumInt
führt die Erweiterung zu int
beim Aufruf der Funktion (1) aus. Hingegen findet die Erweiterung auf sumGen
in dem arithmetischen Ausdruck statt (2).
Die Beispiele dieses Abschnitts zu Lambdas haben das eine oder andere sehr überraschende Detail zur Konvertierungen von Datentypen vorgestellt. Ein typischer Einsatz von Template Lambdas besteht in der Verwendung von Containern in Lambas.
Template-Parameter für Container
Das folgenden Programm stellt Lambdas vor, die einen Container annehmen. Jede Lambda gibt die Länge 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
lässt sich mit jedem Datentyp aufrufen, der die Methode size()
unterstützt. lambdaVector
ist hingegen spezifischer: Sie nimmt nur einen std::vector
an. lambdaVectorIntegral
verwendet das C++20 Concept std::integral
. Damit lässt sich nur ein std::vector
mit integralen Datentypen wie int
verwenden. Um es einzusetzen, muss die Headerdatei <concepts>
inkludiert werden. Ich denke, das kleine Programm erklärt sich selbst.
Das Programm templateLambdaVector.cpp
enthält ein leicht zu übersehendes Feature: Seit C++17 kann der Compiler den Datentyp eines Klassen-Templates von seinen Funktionsargumenten ableiten (1). Daher kann ich statt einem wortreichen std::vector<int> myVec{1, 2, 3}
einfach std::vector myVec{1, 2, 3}
schreiben.
Wie geht's weiter?
In meinem nächsten Artikel geht es um weitere C++20 Verbesserung rund um Lambdas. ( [4])
URL dieses Artikels:
https://www.heise.de/-4860846
Links in diesem Artikel:
[1] https://godbolt.org/
[2] https://cppinsights.io/s/3ec92b0b
[3] https://www.grimm-jaud.de/index.php/blog/category/c-insights
[4] mailto:rainer@grimm-jaud.de
Copyright © 2020 Heise Medien