Podle posledního indexu Tiobe jsou nejpopulárnějšími jazyky Java, C, C++, C# a Objective-C. Je zajímavé porovnat implementaci a omezení lambda výrazů v těchto jazycích, člověk tak snáze porozumí, jak to uvnitř funguje.
Java
Java lambda výrazy nemá (existuje experimentální implementace, objevit se mají v osmé verzi). Má ale anonymní třídy, které je snadno nahradí. S tím se pojí určitá omezení. Chceme-li použít lokální proměnnou uvnitř lambda výrazu, musí být deklarována jako final:
final int n = 0;
new Trida() { public int inc() { return n + 1; }}
Z toho samozřejmě plyne, že takovou proměnnou nemůžeme v anonymní třídě měnit. Toto omezení je dáno implementací, anonymní třídy totiž mohou přežít kontext, v němž byly vytvořeny, tedy po zániku proměnných na zásobníku. Díky final může runtime vytvořit kopii bez nebezpečí, že se data později změní. Pokud potřebujeme mít modifikovatelnou proměnnou, deklarujeme ji jako instanční proměnnou buď v anonymní třídě, nebo v třídě, v jejíž metodě jsme anonymní třídu vytvořili.
Pěkným příkladem použití anonymních tříd je AsyncCallback v GWT.
C
Podporu lambda výrazů má C v clangu (tam se nazývají bloky). Každý blok (zapsaný literálem) se vytvoří na zásobníku a se zásobníkem také zanikne. Pokud chceme blok zachovat (např. předat jako callback asynchronní metodě), musíme jej zkopírovat na haldu pomocí Block_copy (z block.h). Tím přebíráme odpovědnost za jeho odstranění z haldy funkcí Block_release.
Lokální proměnné se kopírují. Pokud chceme nějakou měnit, musí být deklarována pomocí __block. Takto deklarované proměnné jsou uloženy v objektu, který vnitřně reprezentuje blok. Zkopírujeme-li blok na haldu, tyto proměnné se zkopírují s ním. Zde je jistá podobnost s C# (lokální proměnná na haldě), narozdíl od C++ (viz dále) můžeme k modifikovatelným proměnným přistupovat i mimo kontext, v němž byl blok vytvořen.
C++
V clangu můžeme používat bloky z C (viz výše). Instance tříd se pro přístup z bloku kopírují (pokud nepoužijeme __block). V C++ se můžeme odpovědnosti za správu bloku na haldě zbavit jednoduchým wrapperem, tj. třídou, která přetíží operátor () a automaticky volá Block_copy a Block_release (taková třída bude podobná chytrým ukazatelům, Block_copy totiž kopíruje jen ze zásobníku na haldu, na haldě jen bloku zvýší čítač referencí, Block_release jej naopak snižuje, takže blok zanikne, až na něj neexistuje žádný odkaz). Takový wrapper má navíc tu výhodu, že je typu std::function<>, je tedy použitelný v std:.thread, std::async apod. Clangovské bloky lze přímo použít např. v Grand Central Dispatch, ale ne v STL.
Lambda výrazy v C++11 mají jinou syntaxi i implementaci. Jsou v podstatě jen syntaktickým cukrem nad funktory. Každá proměnná, kterou chceme ve výrazu použít, musí být explicitně uvedena (v „capture“ seznamu). Proměnná se buď zkopíruje (a pak ji můžeme použít i mimo kontext, v němž byl výraz vytvořen) nebo se na ni odkazuje referencí (a pokud jde o proměnnou na zásobníku, nelze ji mimo kontext použít). Z tohoto pohledu jsou clangovské bloky univerzálnější. Chceme-li psát přenositelný kód, musíme mít toto omezení na paměti.
C#
C# podporuje lambda výrazy od třetí verze. Modifikovatelné proměnné fungují podobně jako v clangu, ale není nutné uvádět __block nebo nějaký podobný modifikátor. Deklarujeme-li například int n = 0, vytvoří se taková proměnná většinou na zásobníku. Pokud ale překladač zjistí, že se používá v nějakém lambda výrazu, automaticky vytvoří tuto proměnnou na haldě. Pokud se tedy výraz použije i mimo lokální kontext, proměnná stále existuje (na zásobníku by zanikla), a navíc se vyhneme kopírování ze zásobníku na haldu. Jistou nevýhodou může být, že všechny proměnné jsou uvnitř výrazu modifikovatelné.
Objective-C
Pro ObjC platí to samé, co pro clangovské blogy v jazyce C. Je ale třeba dodat, jak se zachází s objekty.
Bloky se chovají jako standardní objekty, tj. místo Block_copy a Block_release můžeme použít zprávy copy a release. Odpovědnosti za správu živostnosti objektu se zbavíme pomocí autorelease: [[blok copy] autorelease] (copy zkopíruje objekt ze zásobníku na haldu a autorelease ho za nás v budoucnu z haldy odstraní).
Při použítí ARC se o bloky na haldě nemusíme starat vůbec, překladačem generované direktivy to udělají za nás.
Poznámka k objektům s čítačem referencí
ObjC a WinRT mají u objektů čítač referencí. Ukazatel na takový objekt se sémanticky chová spíše jako handle v Javě či C#. Při použití s lambda výrazy to znamená, že zatímco neobjektové proměnné se kopírují, objektům se pouze zvýší čítač referencí (a opět se sníží při zániku bloku).
Pokud tedy v C++/CX (WinRT) napíšeme
Trida^ obj = ref new Trida;
[obj](){ obj->neco(); };
instanci třídy Trida se inkrementuje čítač, protože obj je uvedené v capture seznamu lambda výrazu. Pokud tomu chceme zabránit (například se chceme vyhnout cyklu referencí), použijeme [&obj], pak si ale samozřejmě musíme dát pozor na zánik použité instance, lokálně vytvořené objekt nepředaný jinam automaticky zanikne s lokálním kontextem (ref new sice vytváří objekty na haldě, ale handle po odstranění ze zásobníku sníží čítač referencí na nulu a zavolá se destruktor).
V Objective-C se chová čítač referencí u objektů podobně. Je třeba ale mít na paměti, že při použití ARC se čítač referencí aktualizuje i u proměnných deklarovaných pomocí __block, zatímco bez ARC ne. Pokud v ARC chceme zamezit vytvoření cyklu referencí, musíme místo __block použít __weak nebo __unsafe_unretained.
V C++ se jedná sice o syntaxtický cukr, což je v C++ všechno, protože cokoliv v C++ se dá naimplementovat v C, akorát to dá víc práce.
A právě o tom to je. Kdo ještě dnes píše funktory pomocí tříd, tak kolikrát nadává, že se nedá funktor vyrobit v těle funkce, kde je volaný, jakože ostatní třídy takto vytvářet jde. Problém je, že třída uvnitř funkce se nesmí použít jako parametr šablony. Z toho důvodu se funktor často nachází mimo kód, se kterým je pevně spjat a stává se nečitelný. A to pomíjím nutnost psát class, a konstruktor a další věci. Opravdu, jsem rád, že lamba funkce v C++ vznikly.
[7. 12. 2011, 13:16 PH]
Nemyslím si, že by to někomu vadilo. Jediný, co nejde je [const &], takže stejně skončím s ručním seznamem [const A &a, const B &b].
C lambda vyrazy nema! Je to jen nestandardni rozsireni jednoho prekladace! C# ma lambda vyrazy uz od verze 2.0 v podobe anonymnich delegatu. final je po technicke strance v Jave zbytecne, protoze ramec muze byt bez ujmy na obecnosti alokovany na heapu a jde to diky lexikalnimu rozsahu platnosti zjistit behem prekladu a neni potreba nejake kopirovani.
@5: "final je po technicke strance v Jave zbytecne". Ne tak uplne, nutnost mit promennou jako final je dusledek implementace, protoze Java ve skutecnosti hodnoty vsech nelokalnich promennych kopiruje do vnitrni funkce - JVM samotna nema prostedky jak pristupovat do jineho nez aktualniho framu. final je tedy jen "pojistka" aby bylo bezpecne hodnotu kopirovat.
"protoze ramec muze byt bez ujmy na obecnosti alokovany na heapu a jde to diky lexikalnimu rozsahu platnosti zjistit behem prekladu a neni potreba nejake kopirovani."
Teoreticky ano, prakticky to neni tak snadne. Moderni CPU konstruovana pro vykonavani klasickeho Ccka, kde vsechny framy jsou na stacku. Pokud chci rychly kod, frames se musi alokovat na stacku - nebo to alespon hoodne pomuze, JIT tvori kod velmi podobny C kodu. Alokovat neco na heapu, neco na stacku by JIT hodne seslozitilo. Navic, statisticky velmi malo framu prezije svuj vlastni navrat, takze vykonne VM alokuji vsechny framy na stacku a na heap se kopiruji jen pokud je frame referencovat po svem navratu. Toto reseni dava statisticky obecne lepsi vysledky.
CLR rovnez nema primou podporu pro pristup do nelokalniho framu a lambdy resi stejne jako navrhuje @7 (coz je princip tzv Closure Compileru pro novejsi verze SqueakVM/CogVM). Toto sice funguje, na druhou stranu se jedna o jisty "podvod", nebot za cenu jednodussi implementace VM se plati vyssim vypocetnim casem (pole se alokuje, coz nese mnohem vetsi rezii nez alokace na stacku navic tato alokace se provadi vzdy, nezavisle na tom, jestli frame prezije nebo neprezije svuj navrat)
@8 Asi kvůli té režii se v clangu vytváří lambda literál vždy na zásobníku. Oni to do céčka přidali kvůli Grand Central Dispatch, kde o rychlost docela jde. Pokud je nějaký lambda výraz používán i později (třeba někde uložen), tak ta režie naopak nevadí a kopírování ze zásobníku na haldu se v celkovém čase ztratí. Jenže pokud člověk nepoužije nejnovější ObjC (LLVM 3.0), ale "jen" C nebo C++, tak se musí sám postarat o uvolnění z haldy.
Autor se zabývá vývojem kompilátorů a knihoven pro objektově-orientované programovací jazyky.
Přečteno 36 200×
Přečteno 25 361×
Přečteno 23 795×
Přečteno 20 177×
Přečteno 17 874×