C++ Insights – Lambdas

Modernes C++  –  0 Kommentare

Viele Programmierer sind vomm syntaktischen Zucker der Lambdas verwirrt. Die Entzuckerung von Lambdas mit C++ Insights löst meist schnell die Verwirrung auf.

Lambdas in C++ scheinen das neue, interessante Sprachfeature zu sein. Meine Meinung basiert auf allen Problemberichten und E-Mails, die ich bisher für C++ Insights erhalten habe.

Sie erlauben uns, Variablen auf unterschiedliche Weise zu erfassen. Aufgrund ihrer leichten Syntax ist es einfach, neue Funktionen wie Objekte zu erstellen. Falls man noch nie davon gehört haben sollte, sind Lambdas im Wesentlichen Klassen mit einem Anrufoperator. Der von uns bereitgestellte Lambda-Rumpf ist dann der Rumpf des Anrufbetreibers. Für den Anfang ist es das. Eigentlich sind Lambdas einer der Gründe, warum ich C++ Insights erstellt habe. Es kann diese Transformation zeigen, die es zumindest für mich einfacher macht, sie zu erklären. Aber sehen wir selbst:

int main()
{
char c{1};

auto l = [=] () { return c; };

return l();
}
Capture-Optionen

Die Dinge werden interessanter, wenn wir über die Capture-Optionen sprechen. Eine als Referenz erfasste Variable wird zum Referenzmitglied der Lambdas-Klasse. Schauen wir uns die Signatur des Call-Operators genau an. Sie sagt const. Ich kann jedoch immer noch x ändern. Es ist nur eine Referenz und keine konstante Referenz, wie wir in C++ Insights sehen können:

int main()
{
char x{1};

auto l = [&] () {
x = 1;
};
}

Die Dinge ändern sich, wenn wir per Kopie erfassen. Die erfasste Variable ist dann eine Kopie. Nun greift das const, wir können x nicht ändern, ohne der Lambda-Definition mutable hinzuzufügen:

int main()
{
char c{1};

auto l = [=] () { return c; };

auto l2 = [=] () mutable {
c = 1;

return c;
};
}

Wenn wir wissen, dass eine Klasse hinter einem Lambda liegt, können wir uns ein bisschen mehr mit Dingen wie der Größe eines Lambdas befassen. Durch Erfassen als Kopie wird die Größe der Klasse um die Größe jeder erfassten Variablen erhöht und eventuell aufgefüllt. Um einige Bytes zu sparen, ist die Reihenfolge der Captures wichtig, wie wir hier sehen können:

int main()
{
char c;
int x;
char b;

auto l = [=] () mutable {
c = 1,
x = 2;
b = 3;
};

static_assert(sizeof(l) == sizeof(int)*3);

auto l2 = [=] () mutable {
x = 2;
c = 1,
b = 3;
};

static_assert(sizeof(l2) == sizeof(int)*2);
}

Zu beachten ist jedoch, dass die Reihenfolge der Mitglieder in der Klasse nicht festgelegt ist.

Eine andere Sache, auf die man achten sollte, sind Lambdas, die als Referenz erfassen. Die Variablen, auf die diese Referenzen verweisen, müssen gültig sein, wenn das Lambda aufgerufen wird. Andernfalls erhalten Sie eine dangling-Referenz. Hier ist ein Beispiel:

auto Lambda()
{
int x{};

return [&]{ return x; };
}

int main()
{
Lambda()();
}

Ich denke, es wird mit einem Blick auf C++ Insights ziemlich klar:

__lambda_5_10 Lambda()
{
int x = {};

class __lambda_5_10
{
int & x;
public: inline /*constexpr */ int operator()() const
{
return x;
}

public: __lambda_5_10(int & _x)
: x{_x}
{}

} __lambda_5_10{x};

return __lambda_5_10;
}

int main()
{
Lambda().operator()();
}

Das war bereits bekannt? Ausgezeichnet! Dann sind wir entweder vorsichtig oder verwenden Kopien, weil wir dann sicher sind? Okay, schauen wir uns das an. Wie wäre es mit diesem Beispiel eines Lambda:

auto Lambda()
{
int y{};
int* x{&y};

return [=]{ return x; };
}

int main()
{
auto x = Lambda()();
}

Hier erfassen wir nur per Kopie, geben aber dennoch etwas vom Stack zurück. Der Zeiger, den wir kopieren, ist immer noch ein Zeiger, der auf etwas auf dem Stack zeigt, das nach dem Verlassen von Lambda nicht mehr gültig ist.

Tiefere Einsichten

An dieser Stelle haben wir Templates, Variadic-Templates, Fold-Expressions, constexpr if und Lambdas behandelt. Wir haben gesehen, wie uns C++ Insights das Innere oder die Details zeigen kann. Wie wäre es, sie alle zusammen zu verwenden? So etwa:

#include <string>
#include <type_traits>

template <typename... Ts>
std::string stringify(Ts&&... args)
{
auto convert = [](auto&& arg) {
using TT = std::remove_reference_t<decltype(arg)>;
if constexpr(std::is_same_v<TT, std::string>) {
return arg;
} else if constexpr(std::is_array_v< TT >) {
return std::string{arg};
} else {
return std::to_string(arg);
}
};

std::string ret{};
auto concat = [](std::string& dst, auto&& arg) {
dst += arg;
};

(..., concat(ret, convert(args)));

return ret;
}

int main()
{
std::string hello{"hello "};
auto s = stringify("A ", hello, 2, " ", "C++ Insights");

printf("%s\n", s.c_str());
}

Hier haben wir ein variadisches Template, das zwei Lambdas enthält. Das erste (convert) führt die Konvertierung von irgendetwas in einen String durch. Dies ist ähnlich wie im vorherigen Beitrag. Das zweite verkettet alle Argumente. Dann in der Zeile (..., concat (ret, convert (args))); werden alle Argumente durch eine Fold-Expression und mit dem Kommaoperator expandiert. Der Code sieht ordentlich aus und ist einigermaßen klein, zumindest meiner Meinung nach. Wie sieht dieser Code intern aus? Schauen wir uns C++ Insights an. Mit all den Templates wird der Code etwas zu groß, um ihn hier zu zeigen.

Ich möchte an dieser Stelle gerne eine Einschränkung erwähnen. Die beiden Lambdas, convert und concat, sind generische Lambdas. Intern haben sie einen Anrufoperator, der ein Template ist. So können wir den auto-Parameter haben. Leider zeigt C++ Insights diesen Teil noch nicht. Es ist auf der Liste und wird bald behoben werden. Ich denke, dass ist es wert zu wissen.

Erstaunlich, was wir alles tun können, richtig? Ich mag die Art und Weise, wie wir Code mit dem neuesten Standard schreiben können. Gerne biete ich Ihnen auch meine Schulungen an, damit Sie von meinem Wissen profitieren können.

Viel Spaß mit C++ Insights. Wer möchte, kann das Projekt unterstützen, entweder als Patreon oder natürlich auch mit Code-Beiträgen.

Dies ist der letzte Beitrag der Serie hier. Ich hoffe, es hat euch Spaß gemacht, die Artikel zu lesen und das Ihr etwas davon mitnehmen konntet. Nun liegt es an dir, mehr über C++ Insights und die Sprache selbst zu erfahren.