Rozważ ten fragment kodu, który wprowadziliśmy w lekcja 18.3 — Wprowadzenie do algorytmów bibliotek standardowych:
#include <algorithm>
#include <array>
#include <iostream>
#include <string_view>
// Our function will return true if the element matches
bool containsNut(std::string_view str)
{
// std::string_view::find returns std::string_view::npos if it doesn't find
// the substring. Otherwise it returns the index where the substring occurs
// in str.
return str.find("nut") != std::string_view::npos;
}
int main()
{
constexpr std::array<std::string_view, 4> arr{ "apple", "banana", "walnut", "lemon" };
// Scan our array to see if any elements contain the "nut" substring
auto found{ std::find_if(arr.begin(), arr.end(), containsNut) };
if (found == arr.end())
{
std::cout << "No nuts\n";
}
else
{
std::cout << "Found " << *found << '\n';
}
return 0;
}Ten kod przeszukuje tablicę ciągów znaków w poszukiwaniu pierwszego elementu zawierającego podciąg „nut”. W ten sposób daje wynik:
Found walnut
A chociaż działa, można to ulepszyć.
Głównym problemem jest to, że std::find_if wymaga przekazania mu wskaźnika funkcji. Z tego powodu jesteśmy zmuszeni zdefiniować funkcję, która zostanie użyta tylko raz, musi mieć nazwę i musi być umieszczona w zasięgu globalnym (ponieważ funkcji nie można zagnieżdżać!). Funkcja jest też na tyle krótka, że niemal łatwiej jest rozpoznać jej działanie po jednej linijce kodu niż po nazwie i komentarzach.
Lambdy to funkcje anonimowe
A wyrażenie lambda (zwana także lambda lub zamknięcie) pozwala nam zdefiniować anonimową funkcję wewnątrz innej funkcji. Zagnieżdżanie jest ważne, ponieważ pozwala nam zarówno uniknąć zanieczyszczenia przestrzeni nazw, jak i zdefiniować funkcję jak najbliżej miejsca jej użycia (zapewniając dodatkowy kontekst).
Składnia lambd jest jedną z dziwniejszych rzeczy w C++ i wymaga przyzwyczajenia się. Lambdy mają postać:
[ captureClause ] ( parameters ) -> returnType
{
statements;
}
- Klauzula przechwytywania może być pusta, jeśli nie są potrzebne żadne przechwytywania.
- Lista parametrów może być pusta, jeśli nie są wymagane żadne parametry. Można go także całkowicie pominąć, chyba że określono typ zwracany.
- Typ zwracany jest opcjonalny i jeśli zostanie pominięty, zostanie przyjęty
auto(w ten sposób przy użyciu dedukcji typu stosowanej do określenia typu zwracanego). Chociaż wcześniej zauważyliśmy, że należy unikać dedukcji typu dla typów zwracanych przez funkcję, w tym kontekście można z niej skorzystać (ponieważ te funkcje są zazwyczaj trywialne).
Pamiętaj również, że lambdy (będąc anonimowe) nie mają nazwy, więc nie musimy jej podawać.
Na marginesie…
Oznacza to, że trywialna definicja lambdy wygląda następująco:
#include <iostream>
int main()
{
[] {}; // a lambda with an omitted return type, no captures, and omitted parameters.
return 0;
}Przepiszmy powyższy przykład za pomocą lambda:
#include <algorithm>
#include <array>
#include <iostream>
#include <string_view>
int main()
{
constexpr std::array<std::string_view, 4> arr{ "apple", "banana", "walnut", "lemon" };
// Define the function right where we use it.
auto found{ std::find_if(arr.begin(), arr.end(),
[](std::string_view str) // here's our lambda, no capture clause
{
return str.find("nut") != std::string_view::npos;
}) };
if (found == arr.end())
{
std::cout << "No nuts\n";
}
else
{
std::cout << "Found " << *found << '\n';
}
return 0;
}Działa to podobnie jak przypadek wskaźnika funkcji i daje identyczny wynik:
Found walnut
Zauważ, jak podobna jest nasza lambda do naszej funkcji containsNut . Obydwa mają identyczne parametry i ciała funkcji. Lambda nie zawiera klauzuli przechwytującej (wyjaśnimy, czym jest klauzula przechwytująca w następnej lekcji), ponieważ jej nie potrzebuje. Pominęliśmy także końcowy typ powrotu w lambdzie (dla zwięzłości), ale ponieważ operator!= zwraca a bool, nasza lambda również zwróci a bool .
Najlepsza praktyka
Postępując zgodnie z najlepszą praktyką definiowania rzeczy w najmniejszym zakresie i najbliżej pierwszego użycia, lambdy są preferowane w stosunku do normalnych funkcji, gdy potrzebujemy trywialnej, jednorazowej funkcji do zaliczenia jako argument innej funkcji.
Typ lambdy
W powyższym przykładzie zdefiniowaliśmy lambdę dokładnie tam, gdzie była potrzebna. Takie użycie lambdy jest czasami nazywane literałem funkcji.
Jednak pisanie lambdy w tym samym wierszu, w którym jest używana, może czasami sprawić, że kod będzie trudniejszy do odczytania. Podobnie jak możemy zainicjować zmienną wartością literału (lub wskaźnikiem funkcji) do późniejszego użycia, możemy również zainicjować zmienną lambda za pomocą definicji lambda, a następnie użyć jej później. Nazwana lambda wraz z dobrą nazwą funkcji może ułatwić odczytanie kodu.
Na przykład w poniższym fragmencie używamy std::all_of aby sprawdzić, czy wszystkie elementy tablicy są parzyste:
// Bad: We have to read the lambda to understand what's happening.
return std::all_of(array.begin(), array.end(), [](int i){ return ((i % 2) == 0); });Możemy poprawić czytelność tego w następujący sposób:
// Good: Instead, we can store the lambda in a named variable and pass it to the function.
auto isEven{
[](int i)
{
return (i % 2) == 0;
}
};
return std::all_of(array.begin(), array.end(), isEven);Zwróć uwagę, jak dobrze brzmi ostatnia linia: „zwróć, czy wszystko z elementów w array są parzystych”
Kluczowa informacja
Przechowywanie lambdy w zmiennej pozwala nam nadać jej użyteczną nazwę, co może pomóc uczynić nasz kod bardziej czytelnym.
Przechowywanie lambdy w zmiennej umożliwia nam również użycie tej lambdy więcej niż raz.
Ale jaki jest typ lambdy isEven?
Jak się okazuje, lambdy nie mają typu, który możemy jawnie określić używać. Kiedy piszemy lambdę, kompilator generuje unikalny typ tylko dla tej lambdy, która nie jest nam widoczna.
Dla zaawansowanych czytelników
W rzeczywistości lambdy nie są funkcjami (co jest częścią sposobu, w jaki unikają ograniczeń C++, które nie obsługują funkcji zagnieżdżonych). Są specjalnym rodzajem obiektów zwanym funktorem. Funktory to obiekty zawierające przeciążony operator() , który sprawia, że można je wywoływać jak funkcję.
Chociaż nie znamy typu lambdy, istnieje kilka sposobów przechowywania lambdy do wykorzystania po zdefiniowaniu. Jeśli lambda zawiera pustą klauzulę przechwytującą (nie ma nic pomiędzy twardymi nawiasami []), możemy użyć zwykłego wskaźnika do funkcji. std::function lub dedukcja typu za pomocą auto słowa kluczowego również zadziała (nawet jeśli lambda zawiera niepustą klauzulę przechwytującą).
#include <functional>
int main()
{
// A regular function pointer. Only works with an empty capture clause (empty []).
double (*addNumbers1)(double, double){
[](double a, double b) {
return a + b;
}
};
addNumbers1(1, 2);
// Using std::function. The lambda could have a non-empty capture clause (discussed next lesson).
std::function addNumbers2{ // note: pre-C++17, use std::function<double(double, double)> instead
[](double a, double b) {
return a + b;
}
};
addNumbers2(3, 4);
// Using auto. Stores the lambda with its real type.
auto addNumbers3{
[](double a, double b) {
return a + b;
}
};
addNumbers3(5, 6);
return 0;
}Jedynym sposobem użycia rzeczywistego typu lambdy jest użycie auto. auto ma to również tę zaletę, że nie ma narzutu na porównywanie to std::function.
A co jeśli chcemy przekazać lambdę do funkcji? Istnieją 4 opcje:
#include <functional>
#include <iostream>
// Case 1: use a `std::function` parameter
void repeat1(int repetitions, const std::function<void(int)>& fn)
{
for (int i{ 0 }; i < repetitions; ++i)
fn(i);
}
// Case 2: use a function template with a type template parameter
template <typename T>
void repeat2(int repetitions, const T& fn)
{
for (int i{ 0 }; i < repetitions; ++i)
fn(i);
}
// Case 3: use the abbreviated function template syntax (C++20)
void repeat3(int repetitions, const auto& fn)
{
for (int i{ 0 }; i < repetitions; ++i)
fn(i);
}
// Case 4: use function pointer (only for lambda with no captures)
void repeat4(int repetitions, void (*fn)(int))
{
for (int i{ 0 }; i < repetitions; ++i)
fn(i);
}
int main()
{
auto lambda = [](int i)
{
std::cout << i << '\n';
};
repeat1(3, lambda);
repeat2(3, lambda);
repeat3(3, lambda);
repeat4(3, lambda);
return 0;
}W przypadku 1 naszym parametrem funkcji jest a std::function. Jest to fajne, ponieważ możemy wyraźnie zobaczyć, jakie są parametry i typ zwracany przez std::function . Wymaga to jednak niejawnej konwersji lambdy przy każdym wywołaniu funkcji, co wiąże się z pewnym obciążeniem. Ta metoda ma również tę zaletę, że można ją rozdzielić na deklarację (w nagłówku) i definicję (w pliku .cpp), jeśli jest to pożądane.
W przypadku 2 używamy szablonu funkcji z parametrem szablonu typu T. Po wywołaniu funkcji zostanie utworzona instancja funkcji, w której T odpowiada rzeczywistemu typowi lambdy. Jest to bardziej wydajne, ale parametry i typ zwracany T nie są oczywiste.
W przypadku 3 używamy auto z C++20, aby wywołać skróconą składnię szablonu funkcji. Generuje to szablon funkcji identyczny jak w przypadku 2.
W przypadku 4 parametrem funkcji jest wskaźnik funkcji. Ponieważ lambda bez przechwyceń zostanie domyślnie przekonwertowana na wskaźnik funkcji, możemy przekazać do tej funkcji lambdę bez przechwyceń.
Najlepsza praktyka
Przechowując lambdę w zmiennej, jako typ zmiennej użyj auto .
Przekazując lambdę do funkcji:
- Jeśli obsługuje C++20, użyj
autojako typ parametru. - W przeciwnym razie użyj funkcji z parametrem szablonu typu lub
std::functionparametrem (lub wskaźnikiem funkcji, jeśli lambda nie zawiera przechwyceń).
Generyczne lambdy
W większości parametry lambda działają według tych samych zasad, co zwykłe parametry funkcji.
Jedynym godnym uwagi wyjątkiem jest to, że począwszy od C++14 możemy używać auto dla parametrów (uwaga: w C++20 zwykłe funkcje mogą również używać auto dla parametrów). Gdy lambda ma jeden lub więcej auto parametr, kompilator wywnioskowa, jakie typy parametrów są potrzebne na podstawie wywołań lambdy.
Ponieważ lambdy z jednym lub większą liczbą auto parametrem mogą potencjalnie współpracować z wieloma różnymi typami, nazywane są one ogólnymi lambdami.
Dla zaawansowanych czytelników
W przypadku użycia w kontekście lambda, auto to tylko skrót parametru szablonu.
Przyjrzyjmy się ogólnej lambdzie:
#include <algorithm>
#include <array>
#include <iostream>
#include <string_view>
int main()
{
constexpr std::array months{ // pre-C++17 use std::array<const char*, 12>
"January",
"February",
"March",
"April",
"May",
"June",
"July",
"August",
"September",
"October",
"November",
"December"
};
// Search for two consecutive months that start with the same letter.
const auto sameLetter{ std::adjacent_find(months.begin(), months.end(),
[](const auto& a, const auto& b) {
return a[0] == b[0];
}) };
// Make sure that two months were found.
if (sameLetter != months.end())
{
// std::next returns the next iterator after sameLetter
std::cout << *sameLetter << " and " << *std::next(sameLetter)
<< " start with the same letter\n";
}
return 0;
}Wyjście:
June and July start with the same letter
W powyższym przykładzie używamy auto parametrów do przechwytywania naszych ciągów przez const odniesienie. Ponieważ wszystkie typy ciągów umożliwiają dostęp do swoich indywidualnych znaków poprzez operator[], nie musimy się przejmować tym, czy użytkownik przekazuje std::string, ciąg znaków w stylu C lub coś innego. To pozwala nam napisać lambdę, która mogłaby zaakceptować dowolną z nich, co oznacza, że jeśli później zmienimy typ months , nie będziemy musieli przepisywać lambdy.
Jednak auto nie zawsze jest najlepszym wyborem. Rozważ:
#include <algorithm>
#include <array>
#include <iostream>
#include <string_view>
int main()
{
constexpr std::array months{ // pre-C++17 use std::array<const char*, 12>
"January",
"February",
"March",
"April",
"May",
"June",
"July",
"August",
"September",
"October",
"November",
"December"
};
// Count how many months consist of 5 letters
const auto fiveLetterMonths{ std::count_if(months.begin(), months.end(),
[](std::string_view str) {
return str.length() == 5;
}) };
std::cout << "There are " << fiveLetterMonths << " months with 5 letters\n";
return 0;
}Wyjście:
There are 2 months with 5 letters
W tym przykładzie użycie auto wywnioskowałoby typ const char*. Nie jest łatwo pracować z ciągami w stylu C (poza użyciem operator[]). W tym przypadku wolimy jawnie zdefiniować parametr jako a std::string_view, co pozwala nam znacznie łatwiej pracować z danymi źródłowymi (np. możemy zapytać widok łańcucha o jego długość, nawet jeśli użytkownik przekazał tablicę w stylu C).
Constexpr lambdas
Począwszy od C++ 17, lambdy są domyślnie constexpr, jeśli wynik spełnia wymagania wyrażenia stałego. Zwykle wymaga to dwóch rzeczy:
- Lambda nie może zawierać żadnych przechwytów lub wszystkie przechwytywania muszą być constexpr.
- Funkcje wywoływane przez lambdę muszą być constexpr. Należy zauważyć, że wiele standardowych algorytmów bibliotecznych i funkcji matematycznych nie zostało stworzonych jako constexpr aż do C++ 20 lub C++ 23.
W powyższym przykładzie nasza lambda nie byłaby domyślnie constexpr w C++17, ale byłaby w C++20 (ponieważ std::count_if została wykonana constexpr w C++20). Oznacza to, że w C++20 możemy utworzyć fiveLetterMonths constexpr:
constexpr auto fiveLetterMonths{ std::count_if(months.begin(), months.end(),
[](std::string_view str) {
return str.length() == 5;
}) };Ogólne lambdy i zmienne statyczne
W lekcji 11.7 -- Szablon funkcji instancja. Omówiliśmy, że gdy szablon funkcji zawiera statyczną zmienną lokalną, każda funkcja utworzona na podstawie tego szablonu otrzyma własną niezależną statyczną zmienną lokalną. Może to powodować problemy, jeśli nie jest to oczekiwane.
Ogólne lambdy działają w ten sam sposób: dla każdego innego typu, który auto jest generowany, zostanie wygenerowana unikalna lambda.
Poniższy przykład pokazuje, jak jedna ogólna lambda zamienia się w dwie różne lambdy:
#include <algorithm>
#include <array>
#include <iostream>
#include <string_view>
int main()
{
// Print a value and count how many times @print has been called.
auto print{
[](auto value) {
static int callCount{ 0 };
std::cout << callCount++ << ": " << value << '\n';
}
};
print("hello"); // 0: hello
print("world"); // 1: world
print(1); // 0: 1
print(2); // 1: 2
print("ding dong"); // 2: ding dong
return 0;
}Wyjście
0: hello 1: world 0: 1 1: 2 2: ding dong
W powyższym przykładzie definiujemy lambdę, a następnie wywołujemy ją z dwoma różnymi parametrami (parametr literału ciągu i parametr całkowity). Generuje to dwie różne wersje lambdy (jedną z parametrem literału ciągu i jedną z parametrem całkowitym).
W większości przypadków jest to nieistotne. Należy jednak pamiętać, że jeśli ogólna lambda używa statycznych zmiennych czasu trwania, zmienne te nie są współdzielone pomiędzy wygenerowanymi lambdami.
Możemy to zobaczyć w powyższym przykładzie, gdzie każdy typ (literały łańcuchowe i liczby całkowite) ma swoją własną, unikalną liczbę! Chociaż lambdę napisaliśmy tylko raz, wygenerowano dwie lambdy — a każda z nich ma własną wersję callCount. Aby mieć wspólny licznik pomiędzy dwiema wygenerowanymi lambdami, musielibyśmy zdefiniować zmienną globalną lub static zmienną lokalną poza lambdą. Jak wiesz z poprzednich lekcji, zarówno globalne, jak i statyczne zmienne lokalne mogą powodować problemy i utrudniać zrozumienie kodu. Będziemy mogli uniknąć tych zmiennych po omówieniu przechwytywania lambda w następnej lekcji.
Dedukcja typu zwracanego i końcowe typy zwracane
Jeśli używana jest dedukcja typu zwracanego, typ zwracanego lambda jest wyliczany z return-instrukcji wewnątrz lambda, a wszystkie instrukcje return w lambdzie muszą zwracać ten sam typ (w przeciwnym razie kompilator nie będzie wiedział, który z nich preferuję).
Na przykład:
#include <iostream>
int main()
{
auto divide{ [](int x, int y, bool intDivision) { // note: no specified return type
if (intDivision)
return x / y; // return type is int
else
return static_cast<double>(x) / y; // ERROR: return type doesn't match previous return type
} };
std::cout << divide(3, 2, true) << '\n';
std::cout << divide(3, 2, false) << '\n';
return 0;
}To powoduje błąd kompilacji, ponieważ typ zwracany przez pierwszą instrukcję return (int) nie jest zgodny z typem zwracanym przez drugą instrukcję return (double).
W przypadku, gdy zwracamy różne typy, mamy dwie opcje:
- Przeprowadź jawne rzutowanie, aby wszystkie typy zwracane były zgodne, lub
- jawnie określ typ zwracany dla lambdy i pozwól kompilatorowi wykonuj niejawne konwersje.
Drugi przypadek jest zwykle lepszym wyborem:
#include <iostream>
int main()
{
// note: explicitly specifying this returns a double
auto divide{ [](int x, int y, bool intDivision) -> double {
if (intDivision)
return x / y; // will do an implicit conversion of result to double
else
return static_cast<double>(x) / y;
} };
std::cout << divide(3, 2, true) << '\n';
std::cout << divide(3, 2, false) << '\n';
return 0;
}W ten sposób, jeśli kiedykolwiek zdecydujesz się zmienić typ powrotu, wystarczy (zwykle) zmienić tylko typ powrotu lambdy, a nie dotykać treści lambdy.
Obiekty funkcyjne biblioteki standardowej
W przypadku typowych operacji (np. dodawania, negacji lub porównywania) nie trzeba pisać własnych lambd, ponieważ biblioteka standardowa zawiera wiele podstawowych wywoływalnych obiektów, których można zamiast tego użyć. Są one zdefiniowane w <funkcjonalności> .
W poniższym przykładzie:
#include <algorithm>
#include <array>
#include <iostream>
bool greater(int a, int b)
{
// Order @a before @b if @a is greater than @b.
return a > b;
}
int main()
{
std::array arr{ 13, 90, 99, 5, 40, 80 };
// Pass greater to std::sort
std::sort(arr.begin(), arr.end(), greater);
for (int i : arr)
{
std::cout << i << ' ';
}
std::cout << '\n';
return 0;
}Wyjście
99 90 80 40 13 5
Zamiast konwertować naszą greater funkcję na lambdę (co nieco zaciemniłoby jej znaczenie), możemy zamiast tego użyć std::greater:
#include <algorithm>
#include <array>
#include <iostream>
#include <functional> // for std::greater
int main()
{
std::array arr{ 13, 90, 99, 5, 40, 80 };
// Pass std::greater to std::sort
std::sort(arr.begin(), arr.end(), std::greater{}); // note: need curly braces to instantiate object
for (int i : arr)
{
std::cout << i << ' ';
}
std::cout << '\n';
return 0;
}Wyjście
99 90 80 40 13 5
Wnioski
Lambdy, a biblioteka algorytmów może wydawać się niepotrzebnie skomplikowana w porównaniu z rozwiązaniem wykorzystującym pętlę. Jednak ta kombinacja może pozwolić na wykonanie bardzo wydajnych operacji w zaledwie kilku linijkach kodu i może być bardziej czytelna niż pisanie własnych pętli. Co więcej, biblioteka algorytmów oferuje potężną i łatwą w użyciu równoległość, której nie uzyskasz w przypadku pętli. Uaktualnianie kodu źródłowego korzystającego z funkcji bibliotecznych jest łatwiejsze niż aktualizowanie kodu wykorzystującego pętle.
Lambdy są świetne, ale nie zastępują zwykłych funkcji we wszystkich przypadkach. Preferuj zwykłe funkcje dla nietrywialnych przypadków, które można ponownie wykorzystać.
Czas quizu
Pytanie nr 1
Utwórz struct Student przechowujące imię i nazwisko oraz punkty ucznia. Utwórz tablicę uczniów i użyj std::max_element , aby znaleźć ucznia z największą liczbą punktów, a następnie wypisz nazwisko tego ucznia. std::max_element pobiera begin i end listę oraz funkcję, która przyjmuje 2 parametry i zwraca true jeśli pierwszy argument jest mniejszy niż drugi.
Biorąc pod uwagę następującą tablicę
std::array<Student, 8> arr{
{ { "Albert", 3 },
{ "Ben", 5 },
{ "Christine", 2 },
{ "Dan", 8 }, // Dan has the most points (8).
{ "Enchilada", 4 },
{ "Francis", 1 },
{ "Greg", 3 },
{ "Hagrid", 5 } }
};twój program powinien print
Dan is the best student
Pytanie nr 2
Użyj std::sort i lambda w poniższym kodzie, aby posortować pory roku według rosnącej średniej temperatury.
#include <algorithm>
#include <array>
#include <iostream>
#include <string_view>
struct Season
{
std::string_view name{};
double averageTemperature{};
};
int main()
{
std::array<Season, 4> seasons{
{ { "Spring", 285.0 },
{ "Summer", 296.0 },
{ "Fall", 288.0 },
{ "Winter", 263.0 } }
};
/*
* Use std::sort here
*/
for (const auto& season : seasons)
{
std::cout << season.name << '\n';
}
return 0;
}Program powinien wydrukować
Winter Spring Fall Summer

