Kuyruk çağrısı optimizasyonu nedir?


817

Çok basit, kuyruk çağrı optimizasyonu nedir?

Daha spesifik olarak, neden açıklanabileceği, uygulanabileceği ve nerede uygulanmadığı bazı küçük kod parçacıkları nelerdir?


10
TCO kuyruk pozisyonunda bir fonksiyon çağrısını bir gitmeye, bir zıplamaya dönüştürür.
Ness Ness

8
Bu soru tam 8 yıl önce soruldu;)
majelbstoat

Yanıtlar:


755

Kuyruk çağrısı optimizasyonu, bir işlev için yeni bir yığın çerçevesi ayırmaktan kaçınabileceğiniz yerdir, çünkü çağrı işlevi, çağrılan işlevden aldığı değeri döndürür. En yaygın kullanım, kuyruk çağrısı optimizasyonundan yararlanmak için yazılmış bir özyinelemeli fonksiyonun sabit yığın alanını kullanabileceği kuyruk özyineleme'dir.

Şema, herhangi bir uygulamanın bu optimizasyonu sağlaması gerektiğini (JavaScript de ES6 ile başlamaktadır) garanti eden birkaç programlama dilinden biridir , bu yüzden Scheme'deki faktöryel fonksiyonun iki örneği:

(define (fact x)
  (if (= x 0) 1
      (* x (fact (- x 1)))))

(define (fact x)
  (define (fact-tail x accum)
    (if (= x 0) accum
        (fact-tail (- x 1) (* x accum))))
  (fact-tail x 1))

İlk işlev kuyruk özyinelemeli değildir, çünkü özyinelemeli çağrı yapıldığında, işlevin çağrı döndükten sonra sonuçla yapması gereken çarpımı izlemesi gerekir. Bu şekilde, yığın aşağıdaki gibi görünür:

(fact 3)
(* 3 (fact 2))
(* 3 (* 2 (fact 1)))
(* 3 (* 2 (* 1 (fact 0))))
(* 3 (* 2 (* 1 1)))
(* 3 (* 2 1))
(* 3 2)
6

Buna karşılık, kuyruk özyinelemeli faktöriyör için yığın izlemesi aşağıdaki gibi görünür:

(fact 3)
(fact-tail 3 1)
(fact-tail 2 3)
(fact-tail 1 6)
(fact-tail 0 6)
6

Gördüğünüz gibi, her gerçek-tail çağrısı için aynı miktarda veriyi takip etmemiz gerekiyor, çünkü sadece elde ettiğimiz değeri en üste döndürüyoruz. Bu, (aslında 1000000) arayacak olsam bile, (olgu 3) ile aynı miktarda alana ihtiyacım olduğu anlamına gelir. Kuyruk özyinelemeli olmayan gerçekte durum böyle değildir ve bu nedenle büyük değerler bir yığın taşmasına neden olabilir.


99
Bununla ilgili daha fazla bilgi edinmek isterseniz, Bilgisayar Programlarının Yapısı ve Yorumunun ilk bölümünü okumanızı öneririm.
Kyle Cronin

3
Harika cevap, mükemmel açıkladı.
Jonah

15
Açıkçası, kuyruk çağrısı optimizasyonu mutlaka arayanın yığın çerçevesini callees ile değiştirmez, bunun yerine kuyruk pozisyonunda sınırsız sayıda aramanın sadece sınırlı miktarda alan gerektirmesini sağlar. Bkz. Will Clinger'ın
Jon Harrop

3
Bu, yinelemeli işlevleri sabit boşlukta yazmanın bir yolu mu? Çünkü yinelemeli bir yaklaşımla aynı sonuçları elde edemediniz mi?
dclowd9901

5
@ dclowd9901, TCO yinelemeli bir döngü yerine işlevsel bir stil tercih etmenizi sağlar. Zorunlu stili tercih edebilirsiniz. Birçok dil (Java, Python) TCO sunmaz, o zaman işlevsel bir çağrının hafızaya mal olduğunu bilmelisiniz ... ve zorunlu stil tercih edilir.
mcoolive

551

Basit bir örneği inceleyelim: C'de uygulanan faktöriyel fonksiyon.

Açık özyinelemeli tanımla başlıyoruz

unsigned fac(unsigned n)
{
    if (n < 2) return 1;
    return n * fac(n - 1);
}

İşlev dönmeden önceki son işlem başka bir işlev çağrısı ise, bir işlev kuyruk çağrısıyla biter. Bu çağrı aynı işlevi çağırırsa, kuyruk özyinelemelidir.

Olsa fac()ilk bakışta görünüyor kuyruk özyinelemeli aslında ne olur gibi, değil mi

unsigned fac(unsigned n)
{
    if (n < 2) return 1;
    unsigned acc = fac(n - 1);
    return n * acc;
}

yani son işlem işlev çağrısı değil çarpma işlemidir.

Bununla birlikte, fac()biriken değeri ek bir argüman olarak çağrı zincirinden geçirerek ve yalnızca nihai sonucu tekrar dönüş değeri olarak ileterek kuyruk yinelemeli olarak yeniden yazmak mümkündür :

unsigned fac(unsigned n)
{
    return fac_tailrec(1, n);
}

unsigned fac_tailrec(unsigned acc, unsigned n)
{
    if (n < 2) return acc;
    return fac_tailrec(n * acc, n - 1);
}

Şimdi, bu neden faydalı? Kuyruk çağrısından hemen sonra geri döndüğümüz için, işlevi kuyruk konumunda çağırmadan önce önceki yığın çerçevesini atabiliriz veya yinelemeli işlevler durumunda yığın çerçevesini olduğu gibi yeniden kullanabiliriz.

Kuyruk çağrısı optimizasyonu özyinelemeli kodumuzu

unsigned fac_tailrec(unsigned acc, unsigned n)
{
TOP:
    if (n < 2) return acc;
    acc = n * acc;
    n = n - 1;
    goto TOP;
}

Bu içine girilebilir fac()ve biz

unsigned fac(unsigned n)
{
    unsigned acc = 1;

TOP:
    if (n < 2) return acc;
    acc = n * acc;
    n = n - 1;
    goto TOP;
}

eşdeğer

unsigned fac(unsigned n)
{
    unsigned acc = 1;

    for (; n > 1; --n)
        acc *= n;

    return acc;
}

Burada görebileceğimiz gibi, yeterince gelişmiş bir iyileştirici kuyruk yinelemesini yineleme ile değiştirebilir, bu da işlev çağrısı yükünü önlemek ve yalnızca sabit miktarda yığın alanı kullanmaktan çok daha verimlidir.


Bir yığın çerçevesinin tam olarak ne anlama geldiğini açıklayabilir misiniz? Çağrı yığını ve yığın çerçevesi arasında bir fark var mı?
Shasak

10
@Kasahs: Yığın çerçevesi, çağrı yığınının belirli bir (etkin) işleve 'ait' kısmıdır; cf en.wikipedia.org/wiki/Call_stack#Structure
Christoph

1
Sadece okuduktan sonra bu yazıyı okuduktan sonra oldukça yoğun tezahür gördüm 2ality.com/2015/06/tail-call-optimization.html
agm1984

198

TCO (Kuyruk Çağrısı Optimizasyonu), akıllı bir derleyicinin bir işlevi çağırması ve ek yığın alanı almaması işlemidir. Bunun gerçekleşebileceği tek durum, bir f işlevinde yürütülen son komutun , g işlevine bir çağrı olmasıdır (Not: g , f olabilir ). Burada anahtar olmasıdır f basitçe çağırır - artık ihtiyaçları değerlendirmeleri yığın alanı g ve ne olursa o zaman döner gr dönecekti. Bu durumda, g'nin çalıştığı ve f olarak adlandırılan şeye sahip olması gereken değeri döndürdüğü optimizasyon yapılabilir.

Bu optimizasyon, yinelemeli çağrıların patlamak yerine sabit yığın alanı almasını sağlayabilir.

Örnek: bu faktöriyel fonksiyon TCOptimizable değildir:

def fact(n):
    if n == 0:
        return 1
    return n * fact(n-1)

Bu işlev, return deyiminde başka bir işlevi çağırmanın yanı sıra bazı şeyler yapar.

Aşağıdaki fonksiyon TCOptimizable:

def fact_h(n, acc):
    if n == 0:
        return acc
    return fact_h(n-1, acc*n)

def fact(n):
    return fact_h(n, 1)

Bunun nedeni, bu işlevlerin herhangi birinde gerçekleşen son şeyin başka bir işlevi çağırmak olmasıdır.


3
Bütün 'g işlevi f olabilir' şey biraz kafa karıştırıcıydı, ama ne demek istediğini anlıyorum ve örnekler gerçekten netleştirdi. Çok teşekkürler!
majelbstoat

10
Kavramı gösteren mükemmel bir örnek. Seçtiğiniz dilin kuyruk araması ortadan kaldırması veya kuyruk araması optimizasyonu uygulaması gerektiğini dikkate almanız yeterlidir. Python ile yazılmış örnekte, 1000 değerini girerseniz, varsayılan Python uygulaması Kuyruk Özyineleme Yok Etmeyi desteklemediğinden "RuntimeError: maksimum özyineleme derinliği aşıldı" elde edersiniz. Guido'nun neden olduğunu açıklayan bir gönderiye bakın : neopythonic.blogspot.pt/2009/04/tail-recursion-elimination.html .
rmcc

" Tek durum" biraz fazla mutlak; en azından teoride, aynı şekilde optimize veya kuyruk pozisyonunda TRMC de var . (cons a (foo b))(+ c (bar d))
Ness Ness

F ve g yaklaşımınızı kabul edilen cevaptan daha iyi sevdim, belki de matematik insanıyım.
Nithin

Sanırım TCOptimize demek istiyorsun. Hiçbir zaman optimize edilemeyen TCOptimizable infers olduğunu söyleyerek (aslında mümkün olduğunda)
Jacques Mathieu

65

Muhtemelen en iyi üst düzey açıklama kuyruk çağrıları, özyinelemeli kuyruk çağrıları ve kuyruk çağrı optimizasyonu için buldum blog yazısı

"Lanet olsun: Bir kuyruk çağrısı"

Dan Sugalski tarafından. Kuyruk arama optimizasyonunda şunları yazıyor:

Bir an için bu basit işlevi düşünün:

sub foo (int a) {
  a += 15;
  return bar(a);
}

Peki, ya da daha çok dil derleyiciniz ne yapabilirsiniz? Yapabileceği şey, formun kodunu return somefunc();düşük seviyeli diziye dönüştürmektir pop stack frame; goto somefunc();. Örneğimizde, biz buna vasıta önce bar, fookendisini temizler ve sonra yerine çağırmaktan daha barbir alt rutin olarak, bir alt düzey yapmak gotobaşlangıcına çalışmasını bar. Foo'Şimdiye kadar ne zaman, yığının kendini temizlenmiş s bargörünüyor başlar gibi denilen kim foogerçekten çağırdı barve ne zaman baronun değerini verir, doğrudan denilen her kim döndürür fooyerine getirmem daha foosonra onun arayana geri hangi.

Ve kuyruk özyinelemesinde:

Kuyruk özyineleme, bir işlev son işlemi olarak çağrının sonucunu döndürürse gerçekleşir . Kuyruk özyineleme ile başa çıkmak daha kolaydır, çünkü bir yerlerde rastgele bir işlevin başlangıcına atlamak yerine, kendinizin başına geri dönersiniz, bu yapılması basit bir şeydir.

Böylece bu:

sub foo (int a, int b) {
  if (b == 1) {
    return a;
  } else {
    return foo(a*a + a, b - 1);
  }

sessizce dönüşür:

sub foo (int a, int b) {
  label:
    if (b == 1) {
      return a;
    } else {
      a = a*a + a;
      b = b - 1;
      goto label;
   }

Bu açıklama hakkında ne gibi bir zorunluluk dil arka plan (C, C ++, Java) gelenler için kavramak ne kadar özlü ve kolay olduğunu


4
404 hata. Bununla birlikte, hala archive.org'da mevcuttur: web.archive.org/web/20111030134120/http://www.sidhe.org/~dan/…
Tommy

Ben anlamadım, ilk foofonksiyon kuyruk çağrısı optimize değil mi? Bir işlevi yalnızca son adımı olarak çağırıyor ve sadece bu değeri döndürüyor, değil mi?
SexyBeast

1
@ TryinHard belki aklınızdakiler değil, ama bunun ne hakkında olduğuna dair bir fikir vermek için güncelledim. Üzgünüm, tüm makaleyi tekrarlamayacak!
btiernay

2
Teşekkür ederim, bu en çok oylanan şema örneğinden daha basit ve daha anlaşılır (bahsetmiyorum, Şema çoğu geliştiricinin anladığı ortak bir dil değil)
Sevin7

2
Nadiren işlevsel dillere dalan biri olarak, "lehçem" de bir açıklama görmek memnuniyet verici. İşlevsel programcıların kendi dillerinde evangelize olma eğilimi (anlaşılabilir) bir eğilim var, ancak zorunlu dünyadan geldiğimde, kafamı böyle bir cevabın etrafına sarmayı çok daha kolay buluyorum.
James Beninger

15

Öncelikle tüm diller tarafından desteklenmediğini unutmayın.

TCO özel bir özyineleme davasına başvurur. Bunun bir amacı, bir işlevde yaptığınız son şey kendini çağırmaksa (örneğin, kendisini "kuyruk" konumundan çağırıyorsa), bu derleyici tarafından standart özyineleme yerine yineleme gibi davranmak için optimize edilebilir.

Gördüğünüz gibi, normalde özyineleme sırasında, çalışma zamanının tüm özyinelemeli çağrıları takip etmesi gerekir, böylece bir geri döndüğünde önceki çağrıda devam edebilir vb. (Bunun nasıl çalıştığına dair görsel bir fikir edinmek için özyinelemeli bir çağrının sonucunu el ile yazmayı deneyin.) Tüm çağrıları takip etmek alan kaplar, bu da işlev kendini çok çağırdığında önemli hale gelir. Ancak TCO ile, sadece "başlangıca geri dön, sadece bu sefer parametre değerlerini bu yenileriyle değiştir" diyebilir. Bunu yapabilir, çünkü özyinelemeli çağrıdan sonra hiçbir şey bu değerleri ifade etmez.


3
Kuyruk çağrıları, özyinelemesiz işlevler için de geçerli olabilir. Dönmeden önceki son hesaplaması başka bir işleve çağrı olan herhangi bir işlev kuyruk çağrısı kullanabilir.
Brian

Her dil için dil bazında doğru olmayabilir - 64 bit C # derleyicisi kuyruk opcodes ekleyebilir, oysa 32-bit sürüm olmayacaktır; ve F # sürüm derlemesi olacaktır, ancak F # hata ayıklaması varsayılan olarak yapılmayacaktır.
Steve Gilham

3
"TCO özel bir özyineleme davası için geçerlidir". Korkarım bu tamamen yanlış. Kuyruk çağrıları, kuyruk pozisyonundaki herhangi bir çağrı için geçerlidir. Çoğunlukla özyineleme bağlamında tartışılır, ancak özyineleme ile özel olarak ilgisi yoktur.
Jon Harrop

@Brian, yukarıda verilen @btiernay bağlantısına bakın. İlk fooyöntem kuyruk çağrısı optimize edilmemiş mi?
SexyBeast

13

X86 sökme analizi ile GCC minimal çalıştırılabilir örnek

Oluşturulan montaja bakarak GCC'nin bizim için otomatik olarak nasıl kuyruk çağrısı optimizasyonu yapabileceğini görelim.

Bu, https://stackoverflow.com/a/9814654/895245 gibi diğer yanıtlarda bahsedilenlerin , optimizasyonun özyinelemeli işlev çağrılarını bir döngüye dönüştürebileceği konusunda son derece somut bir örnek olacaktır .

Bellek erişimi çoğu zaman günümüzde programları yavaşlatan ana şey olduğundan , bu da bellek tasarrufu sağlar ve performansı artırır .

Bir girdi olarak, GCC'ye optimize edilmemiş saf yığın tabanlı bir faktöriyel veriyoruz:

tail_call.c

#include <stdio.h>
#include <stdlib.h>

unsigned factorial(unsigned n) {
    if (n == 1) {
        return 1;
    }
    return n * factorial(n - 1);
}

int main(int argc, char **argv) {
    int input;
    if (argc > 1) {
        input = strtoul(argv[1], NULL, 0);
    } else {
        input = 5;
    }
    printf("%u\n", factorial(input));
    return EXIT_SUCCESS;
}

GitHub akış yukarı .

Derleyin ve sökün:

gcc -O1 -foptimize-sibling-calls -ggdb3 -std=c99 -Wall -Wextra -Wpedantic \
  -o tail_call.out tail_call.c
objdump -d tail_call.out

nereye -foptimize-sibling-callsgöre kuyruk aramaların genelleme adıdır man gcc:

   -foptimize-sibling-calls
       Optimize sibling and tail recursive calls.

       Enabled at levels -O2, -O3, -Os.

belirtildiği gibi: gcc'nin kuyruk özyineleme optimizasyonu yapıp yapmadığını nasıl kontrol edebilirim?

Ben seçiyorum -O1çünkü:

  • optimizasyon ile yapılmaz -O0. Bunun gerekli ara dönüşümlerin eksik olmasından kaynaklandığından şüpheleniyorum.
  • -O3 kuyruğu da optimize edilmiş olmasına rağmen çok eğitici olmayacak ungodly verimli kod üretir.

İle sökme -fno-optimize-sibling-calls:

0000000000001145 <factorial>:
    1145:       89 f8                   mov    %edi,%eax
    1147:       83 ff 01                cmp    $0x1,%edi
    114a:       74 10                   je     115c <factorial+0x17>
    114c:       53                      push   %rbx
    114d:       89 fb                   mov    %edi,%ebx
    114f:       8d 7f ff                lea    -0x1(%rdi),%edi
    1152:       e8 ee ff ff ff          callq  1145 <factorial>
    1157:       0f af c3                imul   %ebx,%eax
    115a:       5b                      pop    %rbx
    115b:       c3                      retq
    115c:       c3                      retq

İle -foptimize-sibling-calls:

0000000000001145 <factorial>:
    1145:       b8 01 00 00 00          mov    $0x1,%eax
    114a:       83 ff 01                cmp    $0x1,%edi
    114d:       74 0e                   je     115d <factorial+0x18>
    114f:       8d 57 ff                lea    -0x1(%rdi),%edx
    1152:       0f af c7                imul   %edi,%eax
    1155:       89 d7                   mov    %edx,%edi
    1157:       83 fa 01                cmp    $0x1,%edx
    115a:       75 f3                   jne    114f <factorial+0xa>
    115c:       c3                      retq
    115d:       89 f8                   mov    %edi,%eax
    115f:       c3                      retq

İkisi arasındaki en önemli fark şudur:

  • -fno-optimize-sibling-callskullanımları callqtipik olmayan optimize işlev çağrısı,.

    Bu talimat, dönüş adresini yığına doğru iter, dolayısıyla onu artırır.

    Bundan başka, bu versiyonu da yok push %rbxki, iter %rbxyığına .

    GCC bunu yapar edi, çünkü ilk işlev argümanı ( n) olan depolar ebx, sonra çağırır factorial.

    GCC'nin bunu yapması gerekiyor, çünkü factorialyeniyi kullanacak başka bir çağrıya hazırlanıyor edi == n-1.

    ebxBu kayıt callee-kaydedilmiş olduğu için seçer : Bir linux x86-64 işlev çağrısı ile hangi kayıtlar korunur, böylece alt aramafactorial onu değiştirmez ve kaybetmez n.

  • -foptimize-sibling-callsyığınına itme hiçbir talimat kullanmaz: sadece does gotoiçinde atlar factorialtalimatlar jeve jne.

    Bu nedenle, bu sürüm herhangi bir işlev çağrısı olmadan while döngüsüne denktir. Yığın kullanımı sabittir.

Ubuntu 18.10, GCC 8.2'de test edilmiştir.


6

Buraya bak:

http://tratt.net/laurie/tech_articles/articles/tail_call_optimization

Muhtemelen bildiğiniz gibi, özyinelemeli işlev çağrıları bir yığına zarar verebilir; yığın yerinin hızla tükenmesi kolaydır. Kuyruk çağrısı optimizasyonu, sabit yığın alanı kullanan özyinelemeli bir stil algoritması oluşturmanın bir yoludur, bu nedenle büyümez ve büyümez ve yığın hataları alırsınız.


3
  1. Fonksiyonun kendisinde hiçbir goto ifadesi olmadığından emin olmalıyız .. callee fonksiyonundaki son şey olan fonksiyon çağrısı ile ilgilenilir.

  2. Büyük ölçekli özyinelemeler bunu optimizasyonlar için kullanabilir, ancak küçük ölçekte, bir işlev çağrısının kuyruk çağrısı yapmasına yönelik talimat yükü gerçek amacı azaltır.

  3. TCO sonsuza dek çalışan bir işleve neden olabilir:

    void eternity()
    {
        eternity();
    }
    

3 henüz optimize edilmedi. Bu, derleyicinin yinelemeli kod yerine sabit yığın alanı kullanan yinelemeli koda dönüştüğü optimize edilmemiş gösterimdir. TCO, veri yapısı için yanlış özyineleme şemasının kullanılmasının nedeni değildir.
nomen

"TCO, bir veri yapısı için yanlış özyineleme şemasının kullanılmasının nedeni değildir." Lütfen bunun belirtilen durumla nasıl ilgili olduğunu açıklayın. Yukarıdaki örnek, TCO'lu ve TCO'suz çağrı yığını üzerinde çerçevelerin bir örneğinin ayrıldığına işaret etmektedir.
grillSandwich

Travers () için asılsız özyineleme kullanmayı seçtiniz. Bunun TCO ile bir ilgisi yoktu. sonsuzluk kuyruk çağrı konumu olur, ancak kuyruk çağrı konumu gerekli değildir: void eternity () {eternity (); çıkış(); }
nomen

Biz oradayken, "büyük ölçekli özyineleme" nedir? Neden fonksiyona gitmekten kaçınmalıyız? Bu, TCO'ya izin vermek için ne gerekli ne de yeterlidir. Ve hangi talimat yükü? TCO'nun bütün noktası, derleyicinin kuyruk pozisyonundaki fonksiyon çağrısını bir goto ile değiştirmesidir.
nomen

TCO, çağrı yığınında kullanılan alanı optimize etmekle ilgilidir. Büyük ölçekli özyineleme ile, çerçevenin boyutundan bahsediyorum. Her özyineleme gerçekleştiğinde, çağrı yığını üzerinde callee işlevinin üzerinde büyük bir çerçeve ayırmam gerekirse, TCO daha yararlı olur ve bana daha fazla özyineleme düzeyi sağlar. Ancak çerçeve boyutumun daha az olması durumunda, TCO olmadan yapabilirim ve yine de programımı iyi çalıştırabilirim (burada sonsuz özyineleme hakkında konuşmuyorum). Eğer işlevde goto ile bırakılırsanız, "kuyruk" çağrısı aslında kuyruk çağrısı değildir ve TCO uygulanamaz.
grillSandwich

3

Özyinelemeli işlev yaklaşımının bir sorunu vardır. Toplam bellek maliyetimizi O (n) yapan O (n) boyutunda bir çağrı yığını oluşturur. Bu, çağrı yığınının çok büyük hale geldiği ve alan bittiği bir yığın taşma hatasına karşı savunmasız hale getirir.

Kuyruk çağrısı optimizasyonu (TCO) şeması. Uzun bir çağrı yığını oluşturmaktan kaçınmak için özyinelemeli işlevleri optimize edebildiği ve dolayısıyla bellek maliyetinden tasarruf ettiği yerlerde.

TCO gibi birçok dil var (JavaScript, Ruby ve az C), Python ve Java ise TCO yapmıyor.

JavaScript dili :) http://2ality.com/2015/06/tail-call-optimization.html kullanılarak onaylandı


0

İşlevsel bir dilde, kuyruk çağrısı optimizasyonu, bir işlev çağrısı, sonuç olarak kısmen değerlendirilen bir ifadeyi döndürebilir, bu da daha sonra arayan tarafından değerlendirilir.

f x = g x

f 6, g 6'ya indirgenir. Dolayısıyla, uygulama sonuç olarak g 6'yı döndürürse ve o ifadeyi çağırırsa, bir yığın çerçevesini kaydeder.

Ayrıca

f x = if c x then g x else h x.

F 6'yı g 6 veya h 6'ya düşürür. Dolayısıyla, uygulama c 6'yı değerlendirir ve doğru olduğunu tespit ederse,

if true then g x else h x ---> g x

f x ---> h x

Basit bir kuyruksuz arama optimizasyonu yorumlayıcısı şöyle görünebilir,

class simple_expresion
{
    ...
public:
    virtual ximple_value *DoEvaluate() const = 0;
};

class simple_value
{
    ...
};

class simple_function : public simple_expresion
{
    ...
private:
    simple_expresion *m_Function;
    simple_expresion *m_Parameter;

public:
    virtual simple_value *DoEvaluate() const
    {
        vector<simple_expresion *> parameterList;
        parameterList->push_back(m_Parameter);
        return m_Function->Call(parameterList);
    }
};

class simple_if : public simple_function
{
private:
    simple_expresion *m_Condition;
    simple_expresion *m_Positive;
    simple_expresion *m_Negative;

public:
    simple_value *DoEvaluate() const
    {
        if (m_Condition.DoEvaluate()->IsTrue())
        {
            return m_Positive.DoEvaluate();
        }
        else
        {
            return m_Negative.DoEvaluate();
        }
    }
}

Kuyruk çağrısı optimizasyon yorumlayıcısı şöyle görünebilir,

class tco_expresion
{
    ...
public:
    virtual tco_expresion *DoEvaluate() const = 0;
    virtual bool IsValue()
    {
        return false;
    }
};

class tco_value
{
    ...
public:
    virtual bool IsValue()
    {
        return true;
    }
};

class tco_function : public tco_expresion
{
    ...
private:
    tco_expresion *m_Function;
    tco_expresion *m_Parameter;

public:
    virtual tco_expression *DoEvaluate() const
    {
        vector< tco_expression *> parameterList;
        tco_expression *function = const_cast<SNI_Function *>(this);
        while (!function->IsValue())
        {
            function = function->DoCall(parameterList);
        }
        return function;
    }

    tco_expresion *DoCall(vector<tco_expresion *> &p_ParameterList)
    {
        p_ParameterList.push_back(m_Parameter);
        return m_Function;
    }
};

class tco_if : public tco_function
{
private:
    tco_expresion *m_Condition;
    tco_expresion *m_Positive;
    tco_expresion *m_Negative;

    tco_expresion *DoEvaluate() const
    {
        if (m_Condition.DoEvaluate()->IsTrue())
        {
            return m_Positive;
        }
        else
        {
            return m_Negative;
        }
    }
}
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.