Lambda kifejezések

Czirkos Zoltán · 2025.09.02.

Magasabb rendű függvények. Lambda kifejezések. Az std::function függvénysablon. Összetett példa a lambda kifejezések használatára.

A laborokhoz

A laborok mellé minden héten lesz kiírva egy beadandó az admin portálon. Ide óra végén töltsd fel a forráskódokat (*.cpp, *.h)! A feladatokat ezért külön projektben oldd majd meg, ne írd felül a megoldásokat.

Labor otthoni munkában

A labor teljesítéséhez legalább az első négy feladatot meg kell oldani.

1. Függvények, függvény objektumok és lambda kifejezések

Az std::transform() algoritmus egy iterátorokkal megadott tartomány (1-2. paraméter) minden elemére meghív egy függvényt (4. paraméter), és a függvény által adott értékeket egy iterátor által mutatott helyre másolja (3. paraméter). Például az alábbi kódban a vektor minden számából gyököt vonunk (a forrás és a cél tároló itt ugyanaz):

#include <iostream>
#include <cmath>
#include <vector>
#include <algorithm>

int main() {
    std::vector<double> v = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
    
    std::transform(v.begin(), v.end(), v.begin(), sqrt);
    
    for (auto i : v)
        std::cout << i << std::endl;
}

Írd át a programot a következő módokon!

  • Írj doubler() nevű függvényt, amely visszatér a paraméterének duplájával! Duplázd meg ezzel a vektor minden elemét.
  • Írd meg ugyanezt úgy, hogy lambda kifejezést használsz a transform() hívásánál!
  • Írd át úgy a lambda kifejezést, hogy egy felhasználó által megadott számmal tudd megszorozni a tömb összes elemét!
Megoldás
/* Duplázás lambdával */
std::transform(v.begin(), v.end(), v.begin(), [](double x) { return 2*x; });

/* Adott számmal szorzás, lambdával */
double c;
std::cin >> c;
std::transform(v.begin(), v.end(), v.begin(), [c](double x) { return c*x; });

Az std::multiplies<T> osztálysablonból olyan függvényobjektum példányosítható, amely összeszorozza a neki átadott két számot. Pl. std::multiplies<int>{} (2, 3) értéke 6. Az std::bind() függvénnyel ebből készíthető duplázó vagy konstanssal szorzó egyparaméterű függvény is, mégpedig úgy, hogy az egyik paraméterét fixen lekötjük 2-re vagy a kívánt konstansra.

  • Írd meg a duplázást az std::multiplies és az std::bind segítségével!
  • Írd meg a felhasználó által megadott számmal szorzást is így!
Megoldás
/* duplázás. a doubler változóra nem lenne szükség */
auto doubler = std::bind(std::multiplies<double>{}, std::placeholders::_1, 2.0);
std::transform(v.begin(), v.end(), v.begin(), doubler);

/* c-vel szorzás. */
double c;
std::cin >> c;
auto c_multiplies = std::bind(std::multiplies<double>{}, std::placeholders::_1, c);
std::transform(v.begin(), v.end(), v.begin(), c_multiplies);

2. Függvényben függvény: a Hérón-féle gyökvonás

A lambda függvények látják az őt létrehozó függvények lokális változóit. Ez lehetőséget ad arra is, hogy egyszerűen ágyazzunk egymásba függvényeket.

Adott az alábbi Javascript kód. Ez az egyik korábbi laboron megismert Hérón-féle gyökvonást valósítja meg. Írd át ezt C++-ra, a belső függvények helyett auto f = [] { ... }; formában írt lambda függvényeket használva! Ne feledd, az átalakítás csak szintaktikai jellegű lesz; szemantikai változtatásra (pl. paraméterek száma) nincsen szükség.

function heron(x) {
    function good_enough(guess) {
        return Math.abs(guess*guess - x) < 0.001;
    }
    function improve(guess) {
        return (guess + x/guess)/2.0;
    }
    var guess = 1.0;
    while (!good_enough(guess))
        guess = improve(guess);
    return guess;
}
Megoldás
#include <iostream>
#include <cmath>

double heron(double x) {
    auto good_enough = [&] (double guess) {
        return std::abs(guess*guess - x) < 0.001;
    };
    auto improve = [&] (double guess) {
        return (guess + x/guess) / 2.0;
    };
    
    double guess = 1.0;
    while (!good_enough(guess))
        guess = improve(guess);
    return guess;
}

int main() {
    std::cout << heron(2.0);
}

3. Függvény rajzolása

Alább a félév elején, laboron szerepelt függvényrajzoló program C++-os megvalósítása. Másold be ezt egy projektbe! A main() függvényt vizsgáld csak meg először, az lesz fontos.

#include <cmath>
#include <vector>
#include <iostream>
#include <algorithm>

class Page {
  public:
    Page(int w, int h) : w_(w), h_(h), page_(w*h) {
        clear();
    }

    void clear() {
        std::fill(page_.begin(), page_.end(), ' ');
    }

    void print() const {
        for (int y = 0; y < h_; ++y) {
            for (int x = 0; x < w_; ++x)
                std::cout << page_[y*w_ + x];
            std::cout << std::endl;
        }
    }

    void setchar(int x, int y, char c) {
        if (x >= 0 && x < w_ && y >= 0 && y < h_)
           page_[y*w_ + x] = c;
    }

    int get_width() const {
        return w_;
    }

    int get_height() const {
        return h_;
    }

  private:
    int w_, h_;
    std::vector<char> page_;
};
 
 
void plot(Page &p, char c, double (*f)(double)) {
    for (int x = 0; x < p.get_width(); ++x) {
        double fx = (x - p.get_width()/2.0)/4.0;
        double fy = f(fx);
        int y = -(fy * 4.0) + p.get_height()/2;
        p.setchar(x, y, c);
    }
}
 
 
int main() {
    Page p(75, 20);
 
    plot(p, '#', std::sin);
    plot(p, 'x', std::cos);
    p.print();
}

Figyeld meg a plot() függvényt! Ez függvényre mutató pointer típusú paraméterrel veszi át a kirajzolandó függvényt. Az átadott függvénynek így nincs kontextusa, nagyon nehéz paraméterezhető függvényeket csinálni, pl. sin 2x, sin 3x stb.

Írd át úgy a plot() függvényt, hogy a double (*)(double) típusú paraméter helyén kaphasson egy lambda függvényt is! Rajzolj ezzel sin n*x függvényt, ahol n értékét a felhasználó adja meg! Két megoldás van: az egyik esetben a plot() függvénysablonná válik, a másik esetben pedig nem kell annak lennie. Melyik a két megoldás? Próbáld ki mind a kettőt! Melyiknek mi az előnye?

Itt ügyelned kell arra, hogy az std::sin valójában nem egy önálló függvény, hanem egy overload set std::sin(float), std::sin(double) stb. tagokkal. A függvénypointeres paraméter esetén a fordító tudja, hogy ebből a halmazból melyik függvényt kell kiválasztania, hiszen double(*)(double) pointer csak a double paraméterű függvényre mutathat, a float paraméterűre nem. Sablonok esetén ez nincs így, nem fogja tudni, melyiket válassza, mert amíg nem választott függvényt, addig nem tudja feloldani a sablonparamétert sem – ugyanakkor a sablonparaméter feloldását kellene előbb elvégeznie. Ilyenkor egy konverzió segít: (double(*)(double))std::sin.

Megoldás

Sablonként:

template <typename FUNC>
void plot(Page &page, char c, FUNC f) {     // !
    for (int x = 0; x < page.get_width(); ++x) {
        double fx = (x - page.get_width()/2.0)/4.0;
        double fy = f(fx);
        int y = -(fy * 4.0) + page.get_height()/2;
        page.setchar(x, y, c);
    }
}

std::function segítségével, nem sablonként:

void plot(Page &page, char c, std::function<double(double)> f) {    // !
    for (int x = 0; x < page.get_width(); ++x) {
        double fx = (x - page.get_width()/2.0)/4.0;
        double fy = f(fx);
        int y = -(fy * 4.0) + page.get_height()/2;
        page.setchar(x, y, c);
    }
}

A sablont használó változat gyorsabb lehet, mert a megrajzolandó függvény belefordítható a plot() belsejébe (inline). Az std::function()-t használó változat pedig kisebb, mert nem kell minden egyes megrajzolandó függvényhez (függvény típushoz) külön lefordítani. Az std::sin() is működik, mert a sablon példányosodhat függvénypointerrel is (visszakapva az eredeti kódot), és az std::function is képes függvényre mutató pointert becsomagolni.

4. Deklaratív programozás: feladatok a Solver osztályhoz I.

Az előadás Solver osztálya letölthető erről a linkről: solver.cpp, template-esítve. Emlékeztető: ez arra képes, hogy egy érték-n-esekből álló halmazból kiválassza azokat az elemeket, amelyek bizonyos feltételeknek megfelelnek. Például:

int main() {
    Solver<int> s;
    
    /* változók hozzáadása */
    auto a = s.add_variable({1, 2, 3, 4, 5});
    auto b = s.add_variable({1, 2, 3, 4, 5});
    
    /* feltételek hozzáadása */
    s.add_constraint([=] { return a() % 2 == 0; });
    s.add_constraint([=] { return b() > a(); });
    
    /* keresés és megoldások kiírása */
    s.solve([=] {
        std::cout << "a=" << a() << ", b=" << b() << std::endl;
    });
}

Ebben a kódban megkeressük azokat az (a,b)∈{1,2,3,4,5}² számpárokat, amelyeknél a páros, és b>a. A Solver a sablonparaméterében megadott típusú értékekkel dolgozik. Az add_variable() ilyen értékekből álló vektort vesz át paraméterként. A hozzáadott feltételeknek pedig igaz értékkel kell visszatérniük, ha teljesülnek.

Oldd meg az osztály használatával az alábbi feladatokat!

Pitagoraszi számhármasok

Melyek a 100-nál nem nagyobb számokból álló pitagoraszi számhármasok? (Tehát a<b<c és a2+b2=c2.) Ehhez érdemes egy külön programrésszel feltölteni egy vektort a számokkal.

Megoldás
int main() {
    Solver<int> s;
    
    std::vector<int> v;
    for (int i = 1; i <= 100; ++i)
        v.push_back(i);
    
    auto a = s.add_variable(v);
    auto b = s.add_variable(v);
    auto c = s.add_variable(v);
    
    s.add_constraint([=] { return a() < b(); });
    s.add_constraint([=] { return b() < c(); });
    s.add_constraint([=] { return a()*a() + b()*b() == c()*c(); });
    
    s.solve([=] {
        std::cout << a() << ' ' << b() << ' ' << c() << std::endl;
    });
}

Írj olyan lambda kifejezést, amelyik nem kiírja, csak megszámolja, hány ilyen számhármas van! (52 a megoldás.)

Megoldás
/* a számlálót referencia szerint kell! */
int count = 0;
s.solve([&count] { ++count; });
std::cout << count << " megoldás." << std::endl;

Euler feladata

Egy gazda sertést, kecskét és juhot vásárolt, összesen 100 állatot, pontosan 600 aranyért. A sertés darabja 21 arany, a kecskéé 8 arany, a juhoké 3 arany. Hány darabot vett mindegyik állatból? (4 megoldás van, pl. 15, 6, 79 és 0, 60, 40.)

Megoldás
int main() {
    Solver<int> solver;
    
    std::vector<int> v;
    for (int i = 0; i <= 100; ++i)  /* 0 is lehet! */
        v.push_back(i);
    
    auto s = solver.add_variable(v);
    auto k = solver.add_variable(v);
    auto j = solver.add_variable(v);
    
    solver.add_constraint([=] { return s() + k() + j() == 100; });
    solver.add_constraint([=] { return s()*21 + k()*8 + j()*3 == 600; });
    
    solver.solve([=] {
        std::cout << s() << ' ' << k() << ' ' << j() << std::endl;
    });
}

Térkép

Adott a képen látható térkép. Színezzük ki ezt kék, sárga, piros színekkel úgy, hogy a szomszédos országok különféle színűek legyenek, és ha két ország határán < jel van, akkor a két szín ábécé-rendben megadott sorrendben kövesse egymást! (Megoldás: A: sárga, B: piros, C: kék, D: kék, E: piros.)

Megoldás
int main() {
    Solver<std::string> s;
    
    auto a = s.add_variable({"kek", "sarga", "piros"});
    auto b = s.add_variable({"kek", "sarga", "piros"});
    auto c = s.add_variable({"kek", "sarga", "piros"});
    auto d = s.add_variable({"kek", "sarga", "piros"});
    auto e = s.add_variable({"kek", "sarga", "piros"});

    /* a középső ország szomszédai */
    s.add_constraint([=] { return a() != b() && a() != c() && a() != d() && a() != e(); });
    /* körben */
    s.add_constraint([=] { return b() != c() && c() != e() && e() != d() && d() != b(); });
    /* a és b ábécésen */
    s.add_constraint([=] { return a() > b(); });
    /* d és e */
    s.add_constraint([=] { return e() > d(); });
    
    s.solve([=] {
        std::cout << "A: " << a() << std::endl;
        std::cout << "B: " << b() << std::endl;
        std::cout << "C: " << c() << std::endl;
        std::cout << "D: " << d() << std::endl;
        std::cout << "E: " << e() << std::endl;
    });
}

5. Deklaratív programozás: feladatok a Solver osztályhoz II.

Nyolckirálynő-probléma

Oldd meg a nyolckirálnyő-problémát a keretrendszerrel! A jól ismert feladat: egy sakktáblára 8 királynőt kell úgy elhelyezni, hogy egyik se üsse a másikat (se közös sor, se közös oszlop, se közös átló). Összesen 92 megoldás van (ha különbözőnek tekintjük a forgatással egymásba vihető megoldásokat). A keresés egyszerűsítéséhez, gyorsításához a feladat egy részét végezd el fejben: mivel tudod, hogy minden sorban pontosan egy királynő lesz, a pozícióik tárolása helyett inkább tárold azt az egyes királynőknél, hogy az adott királynő (az elsőtől a nyolcadikig) a saját sorában (szintén az elsőtől a nyolcadikig) hányadik oszlopban van!

Megoldás
int main() {
    Solver<int> s;
    
    /* oszlopok 1-től 8-ig */
    std::vector<int> col;
    for (int i = 1; i <= 8; ++i)
        col.push_back(i);

    /* a 8 db királynő hozzáadása */
    std::vector<Solver<int>::VariableFunc> queens;
    for (int q = 0; q < 8; ++q)
        queens.push_back(s.add_variable(col));

    /* a melyik-sorban-hol-a-királynő miatt eleve nincsenek egy sorban */

    /* nem lehetnek egy oszlopban */
    s.add_constraint([=] {
        for (int i = 0; i < 8; ++i)
            for (int j = i+1; j < 8; ++j)
                if (queens[i]() == queens[j]())
                    return false;
        return true;
    });

    /* nem lehetnek egy átlón = a sorkoordináták és az
     * oszlopkoordináták különbsége nem lehet azonos.
     * egészen pontosan, ezek abszolút értékei nem. */
    s.add_constraint([=] {
        for (int i = 0; i < 8; ++i)
            for (int j = i+1; j < 8; ++j)
                if (abs(queens[i]() - queens[j]()) == abs(i-j))
                    return false;
        return true;
    });

    /* megoldások kiírása */
    int count = 0;
    s.solve([=, &count] {
        std::cout << ++count << ". megoldás:" << std::endl;
        /* 8 sor */
        for (int q = 0; q < 8; ++q) {
            auto q_col = queens[q]();
            /* 8 oszlop, ha itt van, akkor X, amúgy . */
            for (int col = 1; col <= 8; ++col)
                std::cout << (col == q_col ? 'X' : '.');
            std::cout << std::endl;
        }
        std::cout << std::endl;
    });
}

Írd át úgy a programot, hogy a 8 helyett mindenhol paramétert használsz, azaz oldd meg a feladatot n×n-es sakktáblára, n királynőre! (Ezt inkább kisebb táblákra próbáld ki. A 9×9-es már nagyon lassú.)

Send more money

 send
+more
─────
money

... ahol minden betű más számjegyet jelöl. (Megoldás: 9567+1085=10652.)

Megoldás
int main() {
    Solver<int> solver;

    auto m = solver.add_variable({0, 1, 2, 3, 4, 5, 6, 7, 8, 9});
    auto s = solver.add_variable({0, 1, 2, 3, 4, 5, 6, 7, 8, 9});
    auto e = solver.add_variable({0, 1, 2, 3, 4, 5, 6, 7, 8, 9});
    auto n = solver.add_variable({0, 1, 2, 3, 4, 5, 6, 7, 8, 9});
    auto d = solver.add_variable({0, 1, 2, 3, 4, 5, 6, 7, 8, 9});
    auto o = solver.add_variable({0, 1, 2, 3, 4, 5, 6, 7, 8, 9});
    auto r = solver.add_variable({0, 1, 2, 3, 4, 5, 6, 7, 8, 9});
    auto y = solver.add_variable({0, 1, 2, 3, 4, 5, 6, 7, 8, 9});

    /* hogy a more és a money elején ne legyen 0 */
    solver.add_constraint([=] {
        return m() != 0;
    });
    /* hogy a send elején ne legyen nulla */
    solver.add_constraint([=] {
        return s() != 0;
    });
    /* mindegyik különbözik */
    auto numz = { s, e, n, d, m, o, r, y };
    solver.add_constraint([=] () -> bool {
        for (auto egyik = numz.begin(); egyik != numz.end(); ++egyik)
            for (auto masik = egyik+1; masik != numz.end(); ++masik)
                if ((*egyik)() == (*masik)())
                    return false;
        return true;
    });
    
    /* egy kis rövidítés... */
    auto send = [=] { return (((s())*10+e())*10+n())*10+d(); };
    auto more = [=] { return (((m())*10+o())*10+r())*10+e(); };
    auto money = [=] { return ((((m())*10+o())*10+n())*10+e())*10+y(); };
    
    /* ... hogy a lényeg aztán ilyen fancy lehessen */
    solver.add_constraint([=] {
        return send() + more() == money();
    });
    
    /* ... és könnyen ki lehessen írni */
    solver.solve([=] {
        std::cout << send() << '+' << more() << '=' << money() << std::endl;
    });
}

6. Függvényhívások naplózása

Az előadáson volt egy példa, amelyben egy matematikai függvényt deriváló C függvény szerepelt. Ez paraméterként egy std::function<double(double)> egyváltozós matematikai függvényt vett át, visszatérési értéke pedig egy ugyanilyen volt; az előbbi deriváltja.

Írj egy olyan függvényt, amelyik visszaad a paramétereként megadott matematikai függvény helyett egy másikat, amely ugyanazt az értéket számítja ki, de kiegészíti a számítást egy naplózási lépéssel! Tehát minden hívásnál kiírja a képernyőre azt, hogy függvényhívás történt.

int main() {
    auto logged_sin = logged_func(sin, "sin");
    std::cout << logged_sin(1.2) << std::endl;
}
Called: sin(1.2)
0.932039
Megoldás
std::function<double(double)> logged_func(std::function<double(double)> func, std::string name) {
    auto logged = [=] (double x) {
        std::clog << "Called: " << name << "(" << x << ")" << std::endl;
        return func(x);
    };
    return logged;
}

7. További feladatok