III. rész: Változók és belső függvények

Czirkos Zoltán · 2022.06.21.

Milyen lehetőségeink vannak a downwards funarg problem megoldására?

Egyes nyelvekben a downwards funarg problem automatikusan megoldott. Vagyis függvények belsejében bármikor definiálhatunk egy másik függvényt; a belső függvény pedig látja a külső függvény paramétereit, lokális változóit. Ezt használjuk ki az alábbi Javascript kódban is, amely Hérón algoritmusával von gyököt:

function heron(x) {
    function good_enough(g) {
        return Math.abs(g*g - x) < 0.001;
    }
    function improve(g) {
        return (g + x/g) / 2.0;
    }
 
    var guess = 1.0;
    while (!good_enough(guess))
        guess = improve(guess);
    return guess;
}

A belső függvényeknek tudniuk kell, hogy mihez képest vizsgálják vagy finomítsák a paraméterként kapott tippet. Az x szám bár nekik nem paraméterük, mégis látják annak értékét.

Milyen lehetőségeink vannak ennek megvalósítására C++-ban?

1. C: paraméter vagy globális változó

C-ben az a fő problémánk, hogy egyáltalán nem lehet ilyen módon függvényeket egymásba ágyazni. Ennek az az oka, hogy épp azt a memóriakezelési problémát nem kívánja megoldani a nyelv, amilyet az ilyen kód okoz – hogy egy függvény nem mindig csak a saját hívása miatt létrejött keretet látja a veremben (stack frame), hanem az őket hívókét is. Ha nem lehetnek egymásban a függvények, akkor pedig nem hivatkozhatnak egymás paramétereire, hisz nincs értelme külső vagy belső függvényről sem beszélni.

Mit tehetünk ilyenkor? Ha megoldhatjuk paraméterátadással is a problémát:

double good_enough(double x, double g) {
    return fabs(g*g - x) < 0.001;
}

double improve(double x, double g) {
    return (g + x/g) / 2.0;
}

double heron(double x) {
    double guess = 1.0;
    while (!good_enough(x, guess))
        guess = improve(x, guess);
    return guess;
}

int main() {
    printf("%f", heron(2));
}

Ehhez persze jelentősen át kell alakítani a kódot, hiszen megváltozik az összes függvény paraméterezése. Vagy dolgozhatunk globális változóval is:

double x;

double good_enough(double g) {
    return fabs(g*g - x) < 0.001;
}

double improve(double g) {
    return (g + x/g)/2.0;
}

double heron(double x_param) {
    x = x_param;
    
    double guess = 1.0;
    while (!good_enough(guess))
        guess = improve(guess);
    return guess;
}

int main() {
    printf("%f", heron(2));
}

Ez bonyolultabb probléma esetén indokolt lehet.

2. C++98: osztályba zárva

Érdekes megfigyelést tehetünk a fenti, globális változós megoldás kapcsán. Kiragadva szerepel alább ennek egy kisebb részlete: a globális változó, és az azt elérő két függvény:

double x;

double good_enough(double g) {
    return fabs(g*g - x) < 0.001;
}

double improve(double g) {
    return (g + x/g)/2.0;
}

Csomagoljuk be ezt egy osztályba! Meglepetésünkre szintaktikailag helyes kódot kapunk:

class Heron {
    double x;

    double good_enough(double g) {
        return fabs(g*g - x) < 0.001;
    }

    double improve(double g) {
        return (g + x/g)/2.0;
    }
};

Egy objektum metódusai ugyanúgy rendelkezhetnek paraméterekkel, mint a globális függvények. Csak ezek az objektum tagváltozóit úgy látják (a rejtett this pointeren keresztül), mintha azok globális változók lennének. Mintha az eddigi két szint, globális és lokális változók szintje közé egy harmadik szint került volna, ahova még adatot tehetünk: globális változók, tagváltozók, lokális változók. A globálisak örökké léteznek; a tagváltozók az objektum élettartamához vannak kötve, a lokálisak pedig a függvényhíváshoz.

Mit jelent ez egy a downwards funarg probléma megoldása szempontjából? Az x változóra csak addig van szükségünk, amíg a gyök kiszámítását végezzük. Kell lennie tehát egy olyan objektumnak, amelyiknek az élettartama ezzel megegyezik. Amíg a számítás fut, az objektum is létezni fog, utána pedig akár el is dobhatjuk:

class Heron {
  public:
    double get(double x_param) {
        x = x_param;
        double g = 1.0;
        while (!good_enough(g))
            g = improve(g);
        return g;
    }

  private:
    double x;
    double good_enough(double g) const {
        return std::abs(g*g - x) < 0.001;
    }
    double improve(double g) const {
        return (g + x/g) / 2.0;
    }
};

int main() {
    Heron h;
    std::cout << h.get(2.0);
}

Ez olyan esetben is tökéletes megoldást jelent, ha a „belső” függvények rekurzívak. Mivel azok mindig csak a this pointert kapják lemásolva, nem pedig az objektumot, így bármilyen mélyre menve ugyanazokat a változókat látjuk. Ez fontos lehet, hiszen a belső függvények a külső függvény lokális változóját akár módosítani is akarhatják:

class CountTreeNodes {
  public:
    int count(Tree *root) {
        nodes = 0;
        do_count(root);
        return nodes;
    }

  private:
    int nodes;
    
    void do_count(Tree *root) {
        if (!root)
            return;
        ++nodes;
        do_count(root->left);
        do_count(root->right);
    }
};

Tulajdonképpen erre az ötletre épül a dependency injection technika is.

3. C++11: lambda függvényekkel

A C++11 óta léteznek lambda függvények; amellett, hogy ezek függvény belsejében is definiálhatóak, értelemszerűen megoldják a memóriakezelési problémát is. Tehát egy belső (lambda) függvény képes látni a külső (őt becsomaoló) függvény változóit:

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

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

Fontos különbség van itt a Javascript, és tipikusan más, automatikus szemétgyűjtéssel rendelkező nyelvekhez képest. Valójában C++-ban a belső függvények nem a külső függvény változóit látják. Ehelyett funktor objektumok jönnek létre, amelyek a fentihez hasonlóan saját tagváltozókkal rendelkeznek. Ezek a tagváltozók pedig vagy másolatai [=] a külső függvény változóinak, vagy pedig olyan referenciák [&], amelyek hivatkoznak azokra. Nem véletlen, hogy jelölnünk kell ezt a lambda kifejezésnél.

4. Hivatkozások