Table of Contents
Lamda Expressions
Στη C++, τα Lambda Expressions εισήχθησαν στο πρότυπο C++11 και αποτελούν έναν σύντομο τρόπο για τη δημιουργία ανώνυμων συναρτήσεων (function objects) απευθείας στο σημείο όπου χρειάζονται. Είναι ιδιαίτερα χρήσιμα όταν θέλουμε να δηλώσουμε μια σύντομη λογική, χωρίς να ορίζουμε επιπλέον συναρτήσεις ή κλάσεις.
Σύνταξη των lamda expressions
Μια λάμδα έκφραση αποτελείται από τέσσερα βασικά μέρη:
[capture clause] (parameters) → return_type { body }
- [Capture Clause]: Καθορίζει ποιες μεταβλητές από το εξωτερικό περιβάλλον είναι προσβάσιμες μέσα στη lamda.
- Parameters(): Οι παράμετροι της συνάρτησης, όπως σε μια κανονική συνάρτηση περικλύονται σε παρένθεση.
- Return Type (προαιρετικό): Επιστρεφόμενος τύπος. Συνήθως ο compiler το συμπεραίνει αυτόματα.
- {Body}: Ο κώδικας που εκτελείται στο σώμα της συνάρτησης. Περικλύεται σε άγκιστρα.
Capture Clause (Δέσμευση Μεταβλητών από το εξωτερικό περιβάλλον της lamda)
Το Capture Clause επιτρέπει στη lamda να “βλέπει” μεταβλητές που ορίστηκαν έξω από αυτήν, χωρίς οι μεταβλητές αυτές να είναι παράμετροι της. Διακρίνουμε τους παρακάτω τύπους Capture Clause.
| Capture | Περιγραφή |
|---|---|
| [ ] | Καμία εξωτερική μεταβλητή δεν είναι προσβάσιμη. |
| [x] | Δέσμευση της μεταβλητής x με τιμή (αντίγραφο). |
| [&x] | Δέσμευση της μεταβλητής x με αναφορά (reference). |
| [=] | Δέσμευση όλων των τοπικών μεταβλητών με τιμή. |
| [&] | Δέσμευση όλων των τοπικών μεταβλητών με αναφορά. |
Παραδείγματα Lamda Expressions
Παράδειγμα 1ο - Φιλτράρισμα και εξαγωγή μέσου όρου
Στο παρακάτω παράδειγμα οι βαθμοί ενός φοιτητής, φιλτράρονται ως προς την προσβασιμοτητας τους και από αυτούς υπολογίζεται άθροισμα και πλήθος, ώστε να εξαχθεί ο μέσος όρος.
- filter_and_average.cpp
#include <iostream> #include <vector> #include <algorithm> int main() { std::vector<int> grades = {70, 85, 40, 92, 55, 30}; int sum = 0; int count = 0; int threshold = 50; // Δέσμευση με αναφορά (&) για να τροποποιούμε τα sum και count // Δέσμευση με τιμή (threshold) γιατί μόνο το διαβάζουμε std::for_each(grades.begin(), grades.end(), [&sum, &count, threshold](int g) { if (g >= threshold) { sum += g; count++; } }); if (count > 0) { std::cout << "Average of passing grades: " << (double)sum / count << std::endl; // Έξοδος: Average of passing grades: 75.5 } }
Παρατηρούμε ότι το Capture Clause λαμβάνει τις μεταβλητές sum, count, threshold. Οι δύο πρώτες καλούνται με αναφορά και την ενημερώνουην μετά την έξοδο από τη lamda, ενώ η threshold καλείται με τιμή.
Πως διαχειρίζεται ο compiler το lambda expression;
Ο compiler μετατρέπει τη λάμδα σε μια ανώνυμη κλάση (anonymous functor). Για την παραπάνω έκφραση lamda ο compiler δημιουργεί την παρακάτω ανώνυμη κλάση.
- lamda2functor.cpp
class __lambda_unique_name { private: int& sum; // Capture by reference int& count; // Capture by reference const int threshold; // Capture by value public: // Constructor για την αρχικοποίηση των captures __lambda_unique_name(int& _sum, int& _count, int _threshold) : sum(_sum), count(_count), threshold(_threshold) {} // Ο τελεστής κλήσης (το σώμα της lambda) // Είναι const εξ ορισμού, εκτός αν χρησιμοποιηθεί η λέξη 'mutable' void operator()(int g) const { if (g >= threshold) { sum += g; count++; } } // Ο compiler διαγράφει τον default constructor __lambda_unique_name() = delete; };
Παρατηρούμε ότι η μεταβλητή threshold δεν αλλάζει μέσα στην έκφραση lambda. Αν θέλαμε να αλλάζει σε διαφορετικές κλήσεις, θα έπρεπε να προσθέσουμε τη λέξη mutable.
Παράδειγμα 2ο - Αθροιστής
Ας δούμε το παρακάτω παράδειγμα που απεικόνίζει έναν αθροιστή και μεταβάλει μία μεταβλητή που έχει περαστεί με τιμή στο Clause.
- sample_accumulator.cpp
#include <iostream> #include <vector> int main() { // Το [total = 0] δημιουργεί μια εσωτερική μεταβλητή (capture by value). auto add_to_total = [total = 0](int amount) mutable { total += amount; // Η τροποποίηση επιτρέπεται μόνο λόγω του 'mutable' return total; }; std::vector<int> expenses = {10, 45, 5, 20}; std::cout << "Starting accumulation..." << std::endl; for (int bill : expenses) { std::cout << "Adding: " << bill << " | Current Total: " << add_to_total(bill) << std::endl; } }
Στο παράδειγμα, η μεταβλητή total μεταβάλει την τιμή της σε κάθε κλίση της έκφρασης lamda. Εδώ η προσθήκη της λέξης mutable είναι απαραίτητη και χωρίς αυτή το πρόγραμμα δεν μεταγλωττίζεται. H λέξη mutable σηματοδοτεί ότι η μεταβλητή στο Capture Clause μπορεί να αλλάξει τιμή. Ο λόγος που συμβαίνει αυτό είναι γιατί η λέξη mutable σηματοδοτεί ότι η συνάρτηση υπερφόρτωσης του τελεστή () δε θα είναι const. Παρακάτω δίνουμε την ανώνυμη κλάση (functor) που παράγεται για τη δεδομένη lambda έκφραση.
- lamda2functor2.cpp
class __lambda_accumulator_unique { private: int total; // Το εσωτερικό state (capture by value) public: // Constructor που αρχικοποιεί το total (στην περίπτωσή μας με 0) __lambda_accumulator_unique(int initial_val) : total(initial_val) {} /** * Ο operator() ΔΕΝ είναι const. * Αυτό συμβαίνει επειδή χρησιμοποιήσαμε τη λέξη 'mutable'. * Αν έλειπε το 'mutable', η μέθοδος θα ήταν: * int operator()(int amount) const { ... } * και η γραμμή total += amount θα έβγαζε error στη μεταγλώττιση. */ int operator()(int amount) { total += amount; // Τροποποιούμε το data member της κλάσης return total; } };
Παράδειγμα 3ο - Password Cheker
Παρακάτω δίνεται ένα πρόγραμμα που διαβάζει από ένα std::vector ένα σύνολο από passwords, εφαρμόζει κάποια κριτήρια για τον αν τα passwords είναι valid ή όχι και εκτυπώνει για κάθε passowrd τη θέση του στη λίστα και τόσα αστεράκια όσα είναι το μέγεθος του. Τα κριτήρια για το αν ένα passowrd είναι valid είναι α) μέγεθος τουλάχιστον 8 χαρκτήρες, β) πρέπει να περιέχει τουλάχιστον ένα πεζό, ένα κεφαλαίο, έναν αριθμό και ένα ειδικό χαρακτήρα.
- password_checker.cpp
#include <iostream> #include <vector> #include <string> #include <algorithm> #include <cctype> //using namespace std; int main() { // Λίστα με passwords για έλεγχο std::vector<std::string> passwords = { "Pass123!", // Valid "weak", // Invalid (μικρό μέγεθος) "OnlyLetters", // Invalid (λείπουν αριθμοί/σύμβολα) "NoSpecial12", // Invalid (λείπει ειδικός χαρακτήρας) "Valid_Pass_2026", // Valid "12345678", // Invalid (λείπουν γράμματα) "Ab1!" // Invalid (πολύ μικρό) }; // Λάμδα για τον έλεγχο εγκυρότητας auto isValid = [specialChars = std::string("!@#$%^&*()-_=+[]{}|;:,.<>?/")](const std::string& p) -> bool { if (p.length() < 8) return false; bool hasUpper = false, hasLower = false, hasDigit = false, hasSpecial = false; for (char c : p) { if (std::isupper(c)) hasUpper = true; else if (std::islower(c)) hasLower = true; else if (std::isdigit(c)) hasDigit = true; else if (specialChars.find(c) != std::string::npos) hasSpecial = true; } return hasUpper && hasLower && hasDigit && hasSpecial; }; std::cout << "------- Checking Passwords -----------" << std::endl; std::cout << "Position | Masked Password | Status" << std::endl; std::cout << "--------------------------------------" << std::endl; for (size_t i = 0; i < passwords.size(); ++i) { bool valid = isValid(passwords[i]); // Εκτύπωση θέσης (i + 1) std::cout << "[" << i + 1 << "] "; // Εκτύπωση αστερίσκων αντί για το password std::string masked(passwords[i].length(), '*'); std::cout << masked; // Ευθυγράμμιση εξόδου (padding) int padding = 15 - (int)passwords[i].length(); for(int j=0; j < (padding > 0 ? padding : 1); ++j) std::cout << " "; std::cout << "| " << (valid ? "VALID" : "INVALID") << std::endl; } return 0; }
Παράδειγμα 4ο - Μετασχηματισμός
Ας υποθέσουμε ότι έχουμε ένα std::vector από αντικείμενα τύπου Student (βλέπε παρακάτω) και θέλουμε να μετασχήματίσουμε τα αντικείμενα αυτά ώστε να κρατήσουμε μόνο τους φοιτητές με ΑΕΜ από 1000 εως 2000 και το αποτέλεσμα να το εισάγουμε σε ένα νέο std::vector ή ένα νέο std::set.
- Student.hpp
#ifndef _STUDENT_HPP_ #define _STUDENT_HPP_ #include <cstring> #include <iostream> class Student { private: char *name; int aem; public: Student(); Student(const char *name, int aem); Student(const Student& ); ~Student(); char* getName() const; int getAEM() const; void setName(char*); void setAEM(int); friend std::ostream& operator<<(std::ostream& out, const Student & st); bool operator>(const Student& st) const; bool operator<(const Student& st) const; Student& operator=(const Student& st); }; Student::Student(const char *name, int aem) { this->name = new char [strlen(name) + 1]; strcpy(this->name, name); this->aem = aem; std::cerr << "Argu Construct : " << *this << std::endl; } Student::Student(const Student& st) { name = new char [strlen(st.name) + 1]; strcpy(name, st.name); aem = st.aem; std::cerr << "Copy Construct : " << *this << std::endl; } Student::Student() { this->name = nullptr; this->aem = 0; } Student::~Student() { if(name != nullptr) { std::cerr << "Destruct: " << *this << std::endl; delete []name; } } char* Student::getName() const { return name; } int Student::getAEM() const { return aem; } void Student::setName(char* name) { this->name != nullptr; delete this->name; this->name = new char [strlen(name) + 1]; strcpy(this->name, name); } void Student::setAEM(int aem) { this->aem = aem; } Student& Student::operator=(const Student& st) { if(name != nullptr) delete name; name = new char [strlen(st.name) + 1]; strcpy(name, st.name); aem = st.aem; std::cerr << "Copy : " << *this << std::endl; return *this; } std::ostream& operator<<(std::ostream& out, const Student& st) { if(st.name != nullptr) out << st.name << " " << st.aem; return out; } bool Student::operator>(const Student& st) const { if(aem > st.aem) return true; return false; } bool Student::operator<(const Student& st) const { if(aem < st.aem) return true; return false; } #endif
- trasfrorm_vector2vector.cpp
#include <iostream> #include <vector> #include <algorithm> // Για το std::copy_if #include <iterator> // Για το std::back_inserter #include "Student.hpp" int main() { // 1. Αρχικό vector με 5 αντικείμενα Student std::vector<Student> students; students.push_back(Student("Giannis", 1050)); students.push_back(Student("Maria", 950)); students.push_back(Student("Kostas", 1500)); students.push_back(Student("Eleni", 2500)); students.push_back(Student("Nikos", 1999)); // 3. Το ΝΕΟ vector όπου θα αποθηκευτούν οι φοιτητές που θα "επιζήσουν" std::vector<Student> filteredStudents; std::cout << "\n--- Φιλτράρισμα με Lambda σε νέο Vector ---\n" << std::endl; // 2. Φιλτράρισμα και αποθήκευση στο νέο vector std::copy_if(students.begin(), students.end(), std::back_inserter(filteredStudents), // Εισαγωγή στο τέλος του νέου vector [](const Student& st) { return st.getAEM() >= 1000 && st.getAEM() <= 2000; }); // Εκτύπωση των αποτελεσμάτων του νέου vector std::cout << "\n--- Φοιτητές στο Νέο Vector ---" << std::endl; for (const auto& student : filteredStudents) { std::cout << student << std::endl; } std::cout << "\n--- Τέλος Προγράμματος ---" << std::endl; return 0; }
- trasfrorm_vector2set.cpp
#include <iostream> #include <vector> #include <set> #include <algorithm> // Απαραίτητο για το std::copy_if #include <iterator> // Απαραίτητο για το std::inserter #include "Student.hpp" int main() { // 1. Δημιουργία vector με 5 αντικείμενα Student std::vector<Student> students; students.push_back(Student("Giannis", 1050)); students.push_back(Student("Maria", 950)); students.push_back(Student("Kostas", 1500)); students.push_back(Student("Eleni", 2500)); students.push_back(Student("Nikos", 1999)); // 3. Το set όπου θα αποθηκευτούν οι φοιτητές που θα "επιζήσουν" std::set<Student> filteredStudents; std::cout << "\n--- Φιλτράρισμα με Lambda και std::copy_if ---\n" << std::endl; // 2. Φιλτράρισμα και αποθήκευση ταυτόχρονα std::copy_if(students.begin(), students.end(), std::inserter(filteredStudents, filteredStudents.end()), [](const Student& st) { return st.getAEM() >= 1000 && st.getAEM() <= 2000; }); // Εκτύπωση των αποτελεσμάτων std::cout << "\n--- Φοιτητές στο Set ---" << std::endl; for (const auto& student : filteredStudents) { std::cout << student << std::endl; } std::cout << "\n--- Τέλος Προγράμματος ---" << std::endl; return 0; }
