Meine Werkzeuge
Namensräume
Varianten

C++/11: Eine Einführung in Lambda Funktionen

Aus indiedev
Version vom 2. April 2014, 04:54 Uhr von Glatzemann (Diskussion | Beiträge)
(Unterschied) ← Nächstältere Version | Aktuelle Version (Unterschied) | Nächstjüngere Version → (Unterschied)
Wechseln zu: Navigation, Suche
Tutorial
C++/11: Eine Einführung in Lambda Funktionen
Autor Roland "Glatzemann" Rosenkranz
Programmier­sprache C++
Kategorie C++
Diskussion Thread im Forum
Lizenz indiedev article license

In diesem Tutorial möchte ich euch ein sehr interessantes, neues Sprachfeature von C++/11 vorstellen: Lambda Funktionen. Auf den ersten Blick erscheinen die anonymen Funktionen, die manchmal auch als Closures bezeichnet werden, kompliziert und unnötig, aber wenn man sich eingearbeitet hat, können sie einem das Leben als Programmierer deutlich erleichtern. Kurze Funktionen können damit genau an der Stelle definiert werden, an der sie auch benötigt werden und verhindern, daß man im Code hin und herspringen muss.

Zunächst möchte ich nun kurz vorstellen, wie der Syntax der Lambdas aussieht und mit diesem neu erworbenen Wissen auch direkt ein sinnvolles Beispiel vorstellen. Dieses Beispiel wird das Verständnis für die Vorteile von Lambdas deutlich hervorheben und das viel besser, als ich es mit vielen Worten beschreiben könnte.

Inhaltsverzeichnis

Syntax

Der Syntax sieht, C++-typisch, ein wenig kryptisch aus, ist aber im Grunde genommen einfach zu verstehen. Ich habe das folgende Bild erstellt, um den Syntax auf einen Blick zu verdeutlichen. Im Bild sind die einzelnen Bestandteile hervorgehoben, wobei zwei der Bestandteile optional sind.

C++11 Lambda Syntax.png

Eine Lambda Funktion wird immer mit der Bindung (die in der Spezifikation capture specification genannt wird) begonnen. Innerhalb der beiden eckigen Klammern der Bindung können Paramter angegeben werden, dazu komme ich aber später noch.

Der nächste Bestandteil ist die optionale Parameterliste. Ist diese leer, werden keine Parameter für die Lambda Funktion verwendet und die Parameterliste kann weggelassen werden.

Direkt darauf folgt ebenfalls der optionale Rückgabewert. Wird dieser nicht angegeben, dann versucht der C++-Compiler selbst den Datentyp herauszufinden, der von der Lambda Funktion zurückgegeben wird. Es muss aber nicht zwingend ein Wert zurückgegeben werden.

Der letzte und wichtigste Bestandteil ist der Funktionsrumpf in geschweiften Klammern (so wie jeder Codeblock in C++). Hier wird der eigentlich Code angegeben, der von der Lambda Funktion ausgeführt wird.

Eine einfache Lambda Funktion ohne Vereinfachungen sieht also wie folgt aus:


#include <iostream>

using namespace std;

int main()
{
	auto func = [] () -> void { cout << "Meine erste Lambda-Funktion ohne Parameter und Rueckgabewert!" << endl; };
	func();	// Lambda Funktion ausführen

	cin.get();	// Auf Tastendruck warten
};

Der Sourcecode ist ausführbar, wenn in Visual Studio 2012 (oder einer ähnlichen IDE, die C++/11 unterstützt) einfach ein leeres Konsolenprojekt erzeugt wird und eine main.cpp hinzugefügt wird, in den der Sourcecode kopiert wird.

In Zeile 7 wird die Lambda Funktion erzeugt. Sowohl die runden Klammern, als auch -> void könnte man weglassen. Werden keine Parameter benötigt und gibt es keinen Rückgabewert (oder ist dieser so einfach, daß der Compiler diesen aus dem Context erkennen kann), dann können diese beiden Elemente weggelassen werden. Der Compiler erkennt den Beginn eines Lambdas also an den eckigen Klammern und am folgenden Codeblock. Die Variable func, in die der Lambda gespeichert wird, ist vom Typ auto. Dies ist ebenfalls eine Neuerung von C++/11. Der Typ wird automatisch ermittelt und wir müssen uns damit nicht rumschlagen. Im Falle der anonymen Funktionen ist dies sehr hilfreich und führt zu kürzerem und übersichtlicherem Code.

In Zeile 8 wird schließlich der Lambda ausgeführt. Der Aufruf ist dabei exakt so, wie man es von jeder anderen Funktion gewohnt ist.

Ich denke, daß mit dieser Beschreibung der grundlegende Syntax der anonymen Funktionen hinreichend erklärt ist und dies auch soweit nachvollziehbar ist. In den folgenden Abschnitten dieses Tutorials möchte ich nun anhand von Beispielen die restlichen Aspekte der Lambda Funktionen erklären. Dabei möchte ich nicht auf die gängigen Beispiele wie Movie-Datenbank oder Adressbuch zurückgreifen, weil das Thema dieser Community schließlich die Spieleentwicklung ist. Ich möchte daher ein Beispiel verwenden, daß diesem Thema näher ist, da dies sicherlich für die Zielgruppe interessanter und einfacher zu lesen ist. Trotzdem kann das hier erworbene Wissen selbstverständlich auch in anderen Bereichen der Entwicklung angewendet werden.

Das Beispiel

Als Beispiel für die weiteren Erklärungen in diesem Tutorial möchte ich eine kleine Container-Klasse entwickeln, in der Spielobjekte (Entities) registriert werden können und mit denen dann - unter Verwendung von Lambda Funktionen - gearbeitet werden kann. Dafür muss ich leider ein wenig ausholen und komme dabei ein wenig vom Thema ab. Der Vorteil ist aber, daß wir am Ende dieses Abschnittes eine Grundlage für die weiteren Erklärungen haben werden. Diese werden dann deutlich kürzer ausfallen und können besser nachvollzogen werden.

Zunächst unsere sehr einfache Entity:


#pragma once

#include <string>

class Entity
{
public:
	Entity(int id, std::string Name, std::string characterClass, int hitpoints)
		: id(id)
		, name(name)
		, characterClass(characterClass)
		, alive(true)
		, hitpoints(hitpoints)
	{
	}

	int id;
	std::string name;
	std::string characterClass;
	bool alive;
	int hitpoints;
};

Der Code sollte soweit selbsterklärend sein.

Als nächstes erzeugen wir uns einen kleinen Container, dem wir Entities hinzufügen können:


#pragma once

#include "Entity.h"
#include <vector>

class EntityContainer
{
public:
	~EntityContainer()
	{
		for (auto e : entities)
		{
			delete e;
		}
	}

	void addEntity(Entity* entity)
	{
		entities.push_back(entity);
	}

private:
	std::vector<Entity*> entities;
};

Ich habe hier aus Vereinfachungsgründen den gesamten Code mit in den Header gepackt. Auch dieser Code sollte soweit verständlich sein.

Zur weiteren Verwendung muss natürlich auch ein Container mit ein paar Entities erzeugt werden. Dies erfolgt schnell und einfach:


int main()
{
	EntityContainer* container = new EntityContainer();
	container->addEntity(new Entity(1, "Stone golem", "Monster", 500));
	container->addEntity(new Entity(2, "Player", "Human", 50));
	container->addEntity(new Entity(3, "Spider", "Monster", 5));
	container->addEntity(new Entity(4, "Bat", "Monster", 10));

    // insert code here

	delete container;
};

Um nun mit dieser Container-Klasse arbeiten zu wollen, möchte ich eine Möglichkeit schaffen, daß man bestimmte Entities abfragen kann. Dabei möchte ich nicht nur einzelne abfragen können, sondern auch ganze Listen. Der klassische Weg dies zu tun wäre nun diverse Methoden in der EntityContainer-Klasse bereitzustellen, die dies vornehmen. Beispielsweise folgende Methoden:


Entity* getEntityById(int id);
Entity* getEntityByName(string name);
vector<Entity*> getEntitiesByName(string name);
vector<Entity*> getEntitiesByNameAndClass(string name, string characterClass);
// weitere Suchfunktionen für alle möglichen Kombinationen

Das führt sehr, sehr schnell zu langem, fehleranfälligem und unübersichtlichem Code. All dies kann man mit Hilfe von Lambda Funktionen vereinfachen. Anstelle von Parametern wird einfach eine Suchfunktion übergeben, die es uns ermöglicht beliebige Parameterkombinationen im Code abzubilden. Dazu entwickeln wir zunächst eine Template-Funktion und fügen diese in den EntityContainer ein:


    template<typename Func>
    std::vector<Entity*> findMatchingEntities (Func func)
    { 
        std::vector<Entity*> results;
        for ( auto itr = entities.begin(), end = entities.end(); itr != end; ++itr )
        {
            // rufe "func" auf um zu prüfen, ob die Suchkriterien passen
            if ( func( *itr ) )
            {
                results.push_back( *itr );
            }
        }

        return results;
    }

Ich habe diese Funktion als Template realisiert, da so für den func-Parameter sowohl eine Lambda Funktion, als auch ein Funktionszeiger oder ein Functor übergeben werden kann. Der Algorithmus ist recht einfach. Es werden einfach alle Einträge unserer Liste mit Entitäten iteriert und für jede wird die (Lambda) Funktion aufgerufen. Wenn diese true zurückliefert - was bedeutet, daß die Suchkriterien passen - dann wird diese Entity in die Ergebnisliste aufgenommen.

Der Aufruf um alle Monster zu ermitteln mit einer Lambda Funktion ist nun sehr einfach:


vector<Entity*> monsters = container->findMatchingEntities( [] (const Entity* e) { return e->characterClass.compare(string("Monster")) == 0; } );

Eine alternative, vielleicht etwas übersichtlichere Schreibweise, diesmal zur Suche nach Menschen, sieht wie folgt aus:


auto humanSearchFunc = [] (const Entity* e) { return e->characterClass.compare(string("Human")) == 0; }; 
vector<Entity*> humans = container->findMatchingEntities( humanSearchFunc );

Wie man das Ganze noch weiter ausfeilen kann, werde ich im weiteren Verlauf bei der Beschreibung der Bindung und der Parameterliste erklären.

Bindung

Bisher haben wir keinerlei Bindung angegeben, was durch die leeren, eckigen Klammern symbolisiert wurde. Die Bindung ist aber trotzdem sehr nützlich, da diese eine Verbindung zwischen dem aufrufenden Kontext und dem Funktionsrumpf der Lambda Funktion herstellt. Daher auch der Name Bindung.

Wozu dies gut ist, ist relativ einfach zu erklären. Unser Such-Lambda für Menschen und Monster ist ja bis auf den Suchstring exakt identisch. Schön wäre es also, wenn man diesen Suchstring einfach dynamisch angeben könnten und genau dies ist möglich, indem der jeweilige Member einfach in der Bindungsliste angegeben wird. Ein Beispiel:


string characterClass = "Human";
auto genericSearchFunc = [characterClass] (const Entity* e) { return e->characterClass.compare(characterClass) == 0; };
vector<Entity*> genericEntities = container->findMatchingEntities( genericSearchFunc );

In Zeile 1 definieren wir ein Member vom Typ String mit dem Wert "Human". Nach diesem String wollen wir suchen. In Zeile 2 wird wie bisher die Lambda Funktion definiert. In der Bindungsliste habe ich aber den Member-Namen characterClass angegeben. Dies führt dazu, daß im Funktionsrumpf der Lambda Funktion auf die Variable characterClass zugegriffen werden kann, die außerhalb des Funktionsrumpfes deklariert wurde.

Natürlicher würde es aussehen, wenn der Suchstring einfach als Parameter übergeben würde. Dies ist aber in diesem Fall leider nicht möglich, da die Parameterliste nicht veränderlich ist. Dies wird klar, wenn wir uns daran erinnern, wie die Funktion innerhalb der Template-Suchfunktion aufgerufen wird. Genau aus diesem Grund gibt es die Bindung.

Für die Bindung existieren die folgenden Modifizierer bzw. Bindungsarten. Dabei bezieht sich die Menge an Membern immer auf den aufrufenden Kontext.

  • [] nichts wird gebunden
  • [&] alle Member werden als Referenz gebunden
  • [&bar] nur der Member bar wird als Referenz gebunden
  • [=] alle Member werden als Kopie gebunden
  • [=, &foo] alle Member werden als Kopie gebunden, mit Ausnahme von foo, welches als Referenz gebunden wird
  • [bar] nur der Member bar wird als Kopie gebunden
  • [this] bindeten den this Zeiger der aufrufenden Klasse

Parameterliste

Die optionale Parameterliste einer Lambda Funktion funktioniert exakt so, wie man es von jeder Funktion oder Methode gewohnt ist. Parameter können als Referenz oder als Kopie übergeben werden und alle Datentypen sind hier erlaubt. Ich gehe davon aus, daß die Parameter von C++ und deren Verwendung hinlänglich bekannt ist, daher verzichte ich hier auf eine weitere Erklärung.

Rückgabewert

Der Typ des Rückgabewerts muss in der Regel nicht angegeben werden, da dieser vom Compiler automatisch bestimmt wird. Manchmal möchte man aber ein implizite Konvertierung des Typs haben und dann ist dieser sehr hilfreich. Auch wenn der Compiler nicht ohne weiteres in der Lage ist, den Rückgabetyp selbständig zu ermitteln, kann dies hilfreich sein.

Fazit

Und schon sind wir am Ende dieses Tutorials angelangt in dem wir gelernt haben, was Lambda Funktionen sind und was man damit so alles anstellen kann. Für den Anfänger dürfte es trotzdem schwierig sein, Einsatzgebiete für Lambda Funktionen zu finden. Aus diesem Grund werde ich in anderen Tutorials auf diesen Artikel verweisen und mit der Zeit werden euch immer mehr Anwendungsmöglichkeiten einfallen.

Ich würde mich freuen, wenn ihr Fragen oder einfach nur ein kurzes Feedback über die Diskussionsfunktion (oben links) im Forum hinterlassen würdet.

Navigation
Tutorials und Artikel
Community Project
Werkzeuge