Τι είναι η έκφραση λάμδα στη C++11;

Τι είναι η έκφραση λάμδα στη C++11; Πότε μπορώ να τη χρησιμοποιήσω; Ποια κατηγορία προβλημάτων επιλύουν που δεν ήταν δυνατή πριν από την εισαγωγή τους;

Μερικά παραδείγματα και περιπτώσεις χρήσης θα ήταν χρήσιμα.

Λύση

Το πρόβλημα

Η C++ περιλαμβάνει χρήσιμες γενικές συναρτήσεις όπως οι std::for_each και std::transform, οι οποίες μπορεί να είναι πολύ χρήσιμες. Δυστυχώς, μπορεί επίσης να είναι αρκετά δυσκίνητες στη χρήση τους, ιδιαίτερα αν ο functor που θέλετε να εφαρμόσετε είναι μοναδικός για τη συγκεκριμένη συνάρτηση.

#include 
#include 

namespace {
  struct f {
    void operator()(int) {
      // do something
    }
  };
}

void func(std::vector& v) {
  f f;
  std::for_each(v.begin(), v.end(), f);
}

Αν χρησιμοποιείτε την f μόνο μία φορά και σε αυτό το συγκεκριμένο σημείο, φαίνεται υπερβολικό να γράφετε μια ολόκληρη κλάση μόνο και μόνο για να κάνετε κάτι ασήμαντο και μοναδικό.

Στη C++03 θα μπορούσατε να μπείτε στον πειρασμό να γράψετε κάτι σαν το παρακάτω, για να διατηρήσετε τον τελεστή τοπικά:

void func2(std::vector& v) {
  struct {
    void operator()(int) {
       // do something
    }
  } f;
  std::for_each(v.begin(), v.end(), f);
}

ωστόσο αυτό δεν επιτρέπεται, το f δεν μπορεί να περάσει σε μια συνάρτηση [template][1] στη C++03.

Η νέα λύση

Η C++11 εισάγει τα lambdas που σας επιτρέπουν να γράψετε έναν inline, ανώνυμο συναρτησιακό για να αντικαταστήσετε το struct f. Για μικρά απλά παραδείγματα αυτό μπορεί να είναι πιο καθαρό στην ανάγνωση (κρατάει τα πάντα σε ένα μέρος) και ενδεχομένως πιο απλό στη συντήρηση, για παράδειγμα στην απλούστερη μορφή:

void func3(std::vector& v) {
  std::for_each(v.begin(), v.end(), [](int) { /* do something here*/ });
}

Οι συναρτήσεις λάμδα είναι απλά συντακτική ζάχαρη για ανώνυμους συναρτησιακούς παράγοντες.

Τύποι επιστροφής

Σε απλές περιπτώσεις ο τύπος επιστροφής της λάμδα προκύπτει για εσάς, π.χ:

void func4(std::vector& v) {
  std::transform(v.begin(), v.end(), v.begin(),
                 [](double d) { return d < 0.00001 ? 0 : d; }
                 );
}

ωστόσο όταν αρχίζετε να γράφετε πιο σύνθετες λάμδα θα συναντήσετε γρήγορα περιπτώσεις όπου ο τύπος επιστροφής δεν μπορεί να εξαχθεί από τον μεταγλωττιστή, π.χ.:

void func4(std::vector& v) {
    std::transform(v.begin(), v.end(), v.begin(),
        [](double d) {
            if (d < 0.0001) {
                return 0;
            } else {
                return d;
            }
        });
}

Για να το επιλύσετε αυτό, σας επιτρέπεται να καθορίσετε ρητά έναν τύπο επιστροφής για μια συνάρτηση λάμδα, χρησιμοποιώντας -> T:

void func4(std::vector& v) {
    std::transform(v.begin(), v.end(), v.begin(),
        [](double d) -> double {
            if (d < 0.0001) {
                return 0;
            } else {
                return d;
            }
        });
}

"Σύλληψη" μεταβλητών

Μέχρι στιγμής δεν'έχουμε χρησιμοποιήσει τίποτα άλλο εκτός από αυτό που έχει περάσει στη λάμδα μέσα σε αυτήν, αλλά μπορούμε να χρησιμοποιήσουμε και άλλες μεταβλητές, μέσα στη λάμδα. Αν θέλετε να αποκτήσετε πρόσβαση σε άλλες μεταβλητές, μπορείτε να χρησιμοποιήσετε τη ρήτρα σύλληψης (το [] της έκφρασης), η οποία μέχρι στιγμής δεν έχει χρησιμοποιηθεί σε αυτά τα παραδείγματα, π.χ.:

void func5(std::vector& v, const double& epsilon) {
    std::transform(v.begin(), v.end(), v.begin(),
        [epsilon](double d) -> double {
            if (d < epsilon) {
                return 0;
            } else {
                return d;
            }
        });
}

Μπορείτε να συλλάβετε τόσο με αναφορά όσο και με τιμή, τις οποίες μπορείτε να καθορίσετε χρησιμοποιώντας & και = αντίστοιχα:

  • [&epsilon] σύλληψη με αναφορά
  • [&] συλλαμβάνει όλες τις μεταβλητές που χρησιμοποιούνται στη λάμδα με αναφορά
  • [=] συλλαμβάνει όλες τις μεταβλητές που χρησιμοποιούνται στη λάμδα με τιμή
  • [&, epsilon] συλλαμβάνει μεταβλητές όπως με [&], αλλά το epsilon με τιμή
  • [=, &epsilon] συλλαμβάνει μεταβλητές όπως με [=], αλλά το epsilon με αναφορά

Ο παραγόμενος operator() είναι const από προεπιλογή, με το συμπέρασμα ότι οι συλλήψεις θα είναι const όταν έχετε πρόσβαση σε αυτές από προεπιλογή. Αυτό έχει ως αποτέλεσμα κάθε κλήση με την ίδια είσοδο να παράγει το ίδιο αποτέλεσμα, ωστόσο μπορείτε να [επισημάνετε τη λάμδα ως mutable][2] για να ζητήσετε ο operator() που παράγεται να μην είναι const.

Σχόλια (14)

Τι είναι η συνάρτηση lambda;

Η έννοια της συνάρτησης λάμδα της C++ προέρχεται από τον λογισμό λάμδα και τον συναρτησιακό προγραμματισμό. Μια λάμδα είναι μια ανώνυμη συνάρτηση που είναι χρήσιμη (στον πραγματικό προγραμματισμό, όχι στη θεωρία) για σύντομα κομμάτια κώδικα που είναι αδύνατο να επαναχρησιμοποιηθούν και δεν αξίζει να ονομαστούν.

Στη C++ μια συνάρτηση λάμδα ορίζεται ως εξής

[]() { } // barebone lambda

ή σε όλο της το μεγαλείο

[]() mutable -> T { } // T is the return type, still lacking throw()

[] είναι ο κατάλογος σύλληψης, () ο κατάλογος επιχειρημάτων και {} το σώμα της συνάρτησης.

Η λίστα σύλληψης

Η λίστα σύλληψης ορίζει τι από το εξωτερικό της λάμδα θα πρέπει να είναι διαθέσιμο μέσα στο σώμα της συνάρτησης και πώς. Μπορεί να είναι είτε:

  1. μια τιμή: [x]
  2. μια αναφορά [&x]
  3. οποιαδήποτε μεταβλητή που βρίσκεται αυτή τη στιγμή στην εμβέλεια με αναφορά [&]
  4. το ίδιο με το 3, αλλά με τιμή [=]

Μπορείτε να συνδυάσετε οποιαδήποτε από τα παραπάνω σε μια λίστα διαχωρισμένη με κόμμα [x, &y].

Ο κατάλογος επιχειρημάτων

Ο κατάλογος των επιχειρημάτων είναι ο ίδιος όπως σε οποιαδήποτε άλλη συνάρτηση της C++.

Το σώμα της συνάρτησης

Ο κώδικας που θα εκτελεστεί όταν κληθεί πραγματικά η lambda.

Αφαίρεση τύπου επιστροφής

Εάν μια λάμδα έχει μόνο μια δήλωση επιστροφής, ο τύπος επιστροφής μπορεί να παραλειφθεί και έχει τον έμμεσο τύπο decltype(return_statement).

Μεταβλητή

Εάν μια λάμδα χαρακτηρίζεται ως μεταλλάξιμη (π.χ. []() mutable { }) επιτρέπεται να μεταλλάσσει τις τιμές που έχουν συλληφθεί από την value.

Περιπτώσεις χρήσης

Η βιβλιοθήκη που ορίζεται από το πρότυπο ISO επωφελείται σε μεγάλο βαθμό από τις λάμδα και ανεβάζει την ευχρηστία αρκετά ψηλά καθώς πλέον οι χρήστες δεν χρειάζεται να γεμίζουν τον κώδικά τους με μικρούς functors σε κάποια προσβάσιμη εμβέλεια.

C++14

Στη C++14 οι lambdas έχουν επεκταθεί με διάφορες προτάσεις.

Αρχικοποιημένες συλλήψεις λάμδα

Ένα στοιχείο της λίστας σύλληψης μπορεί τώρα να αρχικοποιηθεί με =. Αυτό επιτρέπει τη μετονομασία των μεταβλητών και τη σύλληψη με μετακίνηση. Ένα παράδειγμα που λαμβάνεται από το πρότυπο:

int x = 4;
auto y = [&r = x, x = x+1]()->int {
            r += 2;
            return x+2;
         }();  // Updates ::x to 6, and initializes y to 7.

και ένα παρμένο από τη Wikipedia που δείχνει πώς να συλλάβετε με το std::move:

auto ptr = std::make_unique(10); // See below for std::make_unique
auto lambda = [ptr = std::move(ptr)] {return *ptr;};

Γενικές Lambdas

Οι Lambdas μπορούν τώρα να είναι γενικές (το auto θα ήταν ισοδύναμο με το T εδώ αν T ήταν ένα όρισμα προτύπου τύπου κάπου στην περιβάλλουσα εμβέλεια):

auto lambda = [](auto x, auto y) {return x + y;};

Βελτιωμένη αφαίρεση τύπου επιστροφής

Η C++14 επιτρέπει την εξαγωγή τύπων επιστροφής για κάθε συνάρτηση και δεν την περιορίζει σε συναρτήσεις της μορφής return expression;. Αυτό επεκτείνεται και στις lambdas.

Σχόλια (2)

Οι εκφράσεις Lambda χρησιμοποιούνται συνήθως για την ενθυλάκωση αλγορίθμων, ώστε να μπορούν να μεταβιβαστούν σε μια άλλη συνάρτηση. Ωστόσο, είναι δυνατή η εκτέλεση μιας lambda αμέσως μετά τον ορισμό της:

[&](){ ...your code... }(); // immediately executed lambda expression

είναι λειτουργικά ισοδύναμη με

{ ...your code... } // simple code block

Αυτό καθιστά τις εκφράσεις λάμδα ένα ισχυρό εργαλείο για την αναδόμηση πολύπλοκων συναρτήσεων. Ξεκινάτε τυλίγοντας ένα τμήμα κώδικα σε μια συνάρτηση λάμδα, όπως φαίνεται παραπάνω. Η διαδικασία της ρητής παραμετροποίησης μπορεί στη συνέχεια να εκτελεστεί σταδιακά με ενδιάμεσες δοκιμές μετά από κάθε βήμα. Μόλις έχετε παραμετροποιήσει πλήρως το τμήμα κώδικα (όπως φαίνεται από την αφαίρεση του &), μπορείτε να μεταφέρετε τον κώδικα σε μια εξωτερική θέση και να τον κάνετε μια κανονική συνάρτηση.

Ομοίως, μπορείτε να χρησιμοποιήσετε εκφράσεις λάμδα για να αρχικοποιήσετε μεταβλητές με βάση το αποτέλεσμα ενός αλγορίθμου...

int a = []( int b ){ int r=1; while (b>0) r*=b--; return r; }(5); // 5!

Ως ένας τρόπος διαμερισμού της λογικής του προγράμματός σας, μπορεί ακόμη και να σας φανεί χρήσιμο να περάσετε μια έκφραση λάμδα ως όρισμα σε μια άλλη έκφραση λάμδα...

[&]( std::function algorithm ) // wrapper section
   {
   ...your wrapper code...
   algorithm();
   ...your wrapper code...
   }
([&]() // algorithm section
   {
   ...your algorithm code...
   });

Οι εκφράσεις λάμδα σας επιτρέπουν επίσης να δημιουργήσετε ονομαστικές ενεστωμένες συναρτήσεις, οι οποίες μπορεί να είναι ένας βολικός τρόπος αποφυγής διπλής λογικής. Η χρήση επώνυμων λάμδα τείνει επίσης να είναι λίγο πιο εύκολη στα μάτια (σε σύγκριση με τις ανώνυμες inline λάμδα) όταν περνάτε μια μη τετριμμένη συνάρτηση ως παράμετρο σε μια άλλη συνάρτηση. Σημείωση: μην ξεχνάτε την άνω τελεία μετά την κλειστή τεθλασμένη αγκύλη.

auto algorithm = [&]( double x, double m, double b ) -> double
   {
   return m*x+b;
   };

int a=algorithm(1,2,3), b=algorithm(4,5,6);

Αν η μετέπειτα σκιαγράφηση προφίλ αποκαλύψει σημαντική επιβάρυνση αρχικοποίησης για το αντικείμενο της συνάρτησης, μπορεί να επιλέξετε να το ξαναγράψετε ως κανονική συνάρτηση.

Σχόλια (18)