Wyrażenia lambda w szczegółach
Część sekcji Programowanie obiektowe ścieżki C++ w Coddy. Lekcja 85 z 104.
Wyrażenia lambda, wprowadzone w C++11, to funkcje anonimowe, które możesz definiować bezpośrednio w kodzie. Choć widziałeś już podstawowe lambdy używane z algorytmami STL, zrozumienie ich pełnej składni otwiera dostęp do zaawansowanych możliwości przechwytywania zmiennych i kontrolowania sposobu dostępu do nich.
Pełna składnia lambdy wygląda tak: [capture](parameters) mutable -> return_type { body }. Klauzula przechwytywania określa, do których zmiennych zewnętrznych lambda ma dostęp i w jaki sposób:
#include <iostream>
int main() {
int x = 10;
int y = 20;
auto byValue = [x]() { return x * 2; }; // Kopia x
auto byRef = [&y]() { y += 5; }; // Referencja do y
auto allByValue = [=]() { return x + y; }; // Kopiuj wszystko
auto allByRef = [&]() { x++; y++; }; // Przechwyć wszystko przez referencję
auto mixed = [x, &y]() { y += x; }; // Połącz oba sposoby
byRef();
std::cout << y << "\n"; // 25
}Domyślnie zmienne przechwycone przez wartość są wewnątrz lambdy const. Słowo kluczowe mutable pozwala modyfikować te kopie:
int counter = 0;
auto increment = [counter]() mutable {
return ++counter; // Modyfikuje kopię należącą do lambdy
};
std::cout << increment() << "\n"; // 1
std::cout << increment() << "\n"; // 2
std::cout << counter << "\n"; // 0 - oryginał pozostaje bez zmianW C++14 dodano przechwytywanie z inicjalizacją, które pozwala tworzyć nowe zmienne lub przenosić obiekty do lambdy:
auto ptr = std::make_unique<int>(42);
auto lambda = [p = std::move(ptr)]() {
return *p;
}; // Własność przeniesiona do lambdaWyrażenia lambda są szczególnie przydatne w programowaniu obiektowym, gdy trzeba przekazać zachowanie jako parametr — na przykład w przypadku funkcji zwrotnych, niestandardowych komparatorów lub procedur obsługi zdarzeń — bez definiowania oddzielnych obiektów funkcyjnych.
Wyzwanie
ŁatwyZbudujmy system obsługi zdarzeń, który pokazuje możliwości wyrażeń lambda z różnymi trybami przechwytywania. Utworzysz prosty dyspozytor zdarzeń, który przechowuje i wywołuje funkcje zwrotne, pokazując, jak lambdy mogą przechwytywać zewnętrzny stan na różne sposoby.
Rozmieścisz kod w trzech plikach:
EventDispatcher.h: Zdefiniuj klasęEventDispatcher, która zarządza funkcjami zwrotnymi zdarzeń.Dyspozytor powinien przechowywać funkcje zwrotne w
std::vectorelementów typustd::function<void()>. Uwzględnij następujące metody:addCallback(std::function<void()> callback): dodaje funkcję zwrotną do listyfireAll(): wywołuje po kolei wszystkie przechowywane funkcje zwrotneclear(): usuwa wszystkie funkcje zwrotne
Dołącz nagłówki
<functional>i<vector>.EventDispatcher.cpp: Zaimplementuj metody dyspozytora. MetodafireAll()powinna po prostu przejść przez wszystkie funkcje zwrotne i wywołać każdą z nich.main.cpp: Wczytaj dwa wejścia:- Liczbę bazową (liczbę całkowitą)
- Mnożnik (liczbę całkowitą)
Utwórz obiekt
EventDispatcheri pokaż różne techniki przechwytywania w lambdach, dodając trzy funkcje zwrotne:- Lambdę, która przechwytuje liczbę bazową przez wartość i wypisuje:
Base value: [base] - Lambdę, która przechwytuje mnożnik przez referencję, zwiększa go o 1, a następnie wypisuje:
Multiplier after increment: [multiplier] - Modyfikowalną lambdę, która przechwytuje przez wartość zmienną licznika (zainicjalizowaną wartością 0), zwiększa ją przy każdym wywołaniu i wypisuje:
Call count: [counter]
Po dodaniu wszystkich funkcji zwrotnych wywołaj
fireAll()dwa razy, aby zobaczyć, jak różne tryby przechwytywania zachowują się przy wielokrotnych wywołaniach. Pomiędzy dwoma wywołaniamifireAll()wypisz---jako separator.Na koniec, po obu rundach, wypisz końcową wartość zmiennej mnożnika z funkcji main, aby pokazać, jak wpłynęło na nią przechwycenie przez referencję:
Final multiplier: [multiplier]
Na przykład dla wejść 10 i 5:
Base value: 10
Multiplier after increment: 6
Call count: 1
---
Base value: 10
Multiplier after increment: 7
Call count: 1
Final multiplier: 7Dla wejść 42 i 0:
Base value: 42
Multiplier after increment: 1
Call count: 1
---
Base value: 42
Multiplier after increment: 2
Call count: 1
Final multiplier: 2Zwróć uwagę na kluczowe zachowania: przechwycenie przez wartość zachowuje oryginalną, niezmienioną liczbę bazową, przechwycenie przez referencję modyfikuje rzeczywistą zmienną mnożnika w funkcji main (zmiana kumuluje się między wywołaniami), a licznik modyfikowalnej lambdy przyjmuje wartość 1 w każdej rundzie. Dzieje się tak dlatego, że fireAll() iteruje po funkcjach zwrotnych, przekazując je przez wartość: w każdej rundzie wywoływana jest nowa kopia przechowywanego elementu std::function, którego przechwycony licznik zaczyna od 0 i zostaje zwiększony do 1, więc stan samej przechowywanej funkcji zwrotnej nigdy nie ulega zmianie.
Spróbuj swoich sił
#include <iostream>
#include "EventDispatcher.h"
using namespace std;
int main() {
int base;
int multiplier;
cin >> base;
cin >> multiplier;
EventDispatcher dispatcher;
// TODO: Dodaj lambdę, która przechwytuje base PRZEZ WARTOŚĆ
// Powinna wypisać: "Base value: [base]"
// TODO: Dodaj lambdę, która przechwytuje multiplier PRZEZ REFERENCJĘ
// Powinna zwiększyć multiplier o 1, a następnie wypisać: "Multiplier after increment: [multiplier]"
// TODO: Dodaj lambdę MUTABLE, która przechwytuje przez wartość licznik (zainicjalizowany wartością 0)
// Powinna zwiększyć licznik i wypisać: "Call count: [counter]"
// TODO: Wywołaj fireAll(), aby uruchomić wszystkie funkcje zwrotne
// TODO: Wypisz "---" jako separator
// TODO: Ponownie wywołaj fireAll()
// TODO: Wypisz końcową wartość multiplier: "Final multiplier: [multiplier]"
return 0;
}
Ta lekcja zawiera krótki quiz. Zacznij lekcję, żeby na niego odpowiedzieć i śledzić swoje postępy.
Wszystkie lekcje w sekcji Programowanie obiektowe
1Podstawy programowania obiektowego
Pliki zewnętrzneBudowanie i kompilacja C++Pliki nagłówkowe i pliki źródłowePrzestrzenie nazw i zakresWprowadzenie do programowania obiektowego w C++Klasy a obiektyWskaźnik „this”Metody (funkcje składowe)Atrybuty (składowe danych)Podstawy konstruktorów i destruktorówPowtórzenie – prosty kalkulator4Właściwości klas
Elementy instancji a elementy statyczneGettery i setteryStałe funkcje składoweSłowo kluczowe mutableMetody i zmienne statyczneFunkcje i klasy zaprzyjaźnionePodsumowanie – menedżer konta bankowego7Dziedziczenie
Podstawy dziedziczeniaPoziomy dostępu w dziedziczeniuKolejność wywołań konstruktora i destruktoraNadpisywanie metodFunkcje wirtualne i VTableWielokrotne dziedziczenieDziedziczenie wirtualnePowtórzenie — hierarchia pracowników2Zarządzanie pamięcią
Pamięć stosu a stertyWskaźniki i referencjePamięć dynamiczna (new/delete)Inteligentne wskaźniki w C++RAII w C++Podsumowanie — menedżer tablicy dynamicznej5Hermetyzacja
Specyfikatory dostępu w C++Specyfikatory dostępu — szczegółowoUkrywanie informacjiStruktura a klasaKlasy zagnieżdżone i wewnętrznePodsumowanie — system ewidencji studentów8Polimorfizm
Polimorfizm czasu kompilacji i wykonaniaPrzeciążanie funkcjiFunkcje wirtualne — powtórkaCzysto wirtualne funkcjeKlasy abstrakcyjneProjektowanie interfejsów w C++Rzutowanie dynamiczne i RTTIPodsumowanie — kalkulator kształtów3Konstruktory i destruktory
Konstruktor domyślnyKonstruktor z parametramiKonstruktor kopiującyKonstruktor przenoszącyListy inicjalizacyjne konstruktoraKonstruktory delegująceDestruktor — szczegółowe omówienieZasada trzech / pięciu / zeraPodsumowanie — klasa String6Przeciążanie operatorów
Wprowadzenie do przeciążania operatorówPrzeciążanie operatorów arytmetycznychPrzeciążanie operatorów porównaniaOperatory strumieniowePrzeciążanie operatora przypisaniaPrzeciążanie operatorów [] i ()Operatory konwersji typówPodsumowanie — klasa Matrix9Szablony
Szablony funkcjiSzablony klasSpecjalizacja szablonówSzablony wariadycznePodstawy SFINAE i cech typówPodsumowanie — kontener generyczny12Nowoczesne funkcje C++
Semantyka przenoszenia i r-wartościDoskonałe przekazywanieWyrażenia lambda w szczegółachstd::function i std::bindconstexpr i constevalWiązania strukturalneoptional, variant, anyPoćwicz samodzielnie: Kompilator C++ online