Türden bağımsız bir toplama algoritması

Bu yazıda C++'ın şablon olanağı kullanılarak yazılmış bir algoritma örneği vereceğim. Bazı durumlarda çok zorlu olabilen ileri şablon uygulamalarına değinmeyeceğim. (Zaten bir noktadan fazlasını ben de bilmiyorum :) )

C++'ın şablonları (templates) sayesinde, türden bağımsız algoritmalar ve veri yapıları oluşturabiliriz.

Şablonların ve türden bağımsız programlamanın ne olduğunu göstermek için, önce şablon olanağı olmayan C dilinde yazılmış bir algoritma vereyim.

'toplam' adındaki bu algoritma, kendisine gösterilen bir dizi tamsayının toplamını döndürüyor olsun:

int toplam(int const * bas, int const * son)
{
    int sonuc = 0;

    for ( ; bas != son; ++bas)
    {
        sonuc += *bas;
    }
    
    return sonuc;
}

Bu algoritmada iki noktaya değinmek istiyorum.

Birincisi, 'son' adlı parametrenin toplamları alınacak sonuncu sayıyı göstermediğine dikkat edin. 'son', aslında sonuncu sayıdan bir sonraki yeri gösteriyor. Bir başka deyişle, '*bas' (baş) sayı dizisinin içinde kabul ediliyor ama '*son' dizinin dışında kabul ediliyor.

Toplamları alınacak sayıları şöyle gösterebiliriz:

 -----------------------------------
| bas | bas + 1 |   ...   | son - 1 | son
 -----------------------------------

Yani 'son', sayıların dışında kalıyor...

İkinci nokta da, dönüş türü olarak int seçmenin pek akıllıca olmadığı. Onun yerine, duruma göre long kullanmak daha iyi olabilirdi. Ben şimdilik, dönüş türünün toplamları alınan sayılarla aynı türden olmasının uygun olduğunu varsayacağım.

İşte yukarıdaki algoritmayı kullanan bir C programı:

#include <stdio.h>

int toplam(int const * bas, int const * son)
{
    int sonuc = 0;

    for ( ; bas != son; ++bas)
    {
        sonuc += *bas;
    }
    
    return sonuc;
}

#define OGE_SAYISI(x) (sizeof(x) / sizeof(*(x)))

int main()
{
    int const sayilar[] = { 7, 12, 30, 42 };
    int const sonuc = toplam(sayilar,
                             sayilar + OGE_SAYISI(sayilar));
    printf("%d\n", sonuc);
}

Program kullanılmaya başladıktan bir süre sonra aynı algoritmanın kesirli sayılar için de gerektiğini düşünelim. Bu sefer aynı algoritmayı double türü için de yazmak zorunda kalırız. C'de aynı adda birden fazla işlev bulunamayacağı için de ya yalnızca double algoritmasının, ya da tutarlı olsun diye her iki algoritmanın adlarını toplamlarını aldıkları sayıların türlerini gösterecek şekilde değiştirmek gerekir:

int toplam_int(int const * bas, int const * son)
{
    /* ... */
}

double toplam_double(double const * bas, double const * son)
{
    double sonuc = 0;

    for ( ; bas != son; ++bas)
    {
        sonuc += *bas;
    }
    
    return sonuc;
}

Sonuçta kod tekrarı yapmış olduk. Kod tekrarının zararları başka bir kısa yazının konusu olabilir; şimdilik kod tekrarının çok kötü bir şey olduğunu söyleyeceğim. :)

Kod tekrarının önüne geçmenin bir yolu, algoritmaları makrolar halinde yazmaktır. Makroların zararları da başka bir kısa yazının konusu olabilir; şimdilik makroların da kötü şeyler olduklarını söyleyeceğim. :)

C++'ın türden bağımsız programlama olanağı, bizi bu kod tekrarından kurtarır. Ayrıca, yine C++'ın işleç yükleme olanağının da (operator overloading) yardımıyla, yazdığımız algoritmaların herhangi bir kullanıcı türü için de kullanılabilmesini sağlar.

Şimdi C++'a geçelim ve toplam algoritmasını şablon halinde yazalım:

#include <iostream>

using std::cout;

template <class Tur>
Tur toplam(Tur const * bas, Tur const * son)
{
    Tur sonuc = 0;

    for ( ; bas != son; ++bas)
    {
        sonuc += *bas;
    }
    
    return sonuc;
}

#define OGE_SAYISI(x) (sizeof(x) / sizeof(*(x)))

int main()
{
    int    const tamsayilar[] = { 7,   12,    30,    42 };
    double const kesirliler[] = { 7.7, 12.12, 30.30, 42.42 };

    cout << toplam(tamsayilar,
                   tamsayilar + OGE_SAYISI(tamsayilar)) << '\n';

    cout << toplam(kesirliler,
                   kesirliler + OGE_SAYISI(kesirliler)) << '\n';
}

Yukarıdaki koddaki 'template' anahtar sözcüğü, 'toplam'ın bir şablon olduğunu bildiriyor. 'template' sözcüğünden sonra gelen <class Tur> ise, bu işlev şablonunda kullanılan bir türün bu şablon yazıldığı sırada bilinmediğini ve şablonun kullanıldığı durumlara bağlı olarak herhangi bir türden olabileceğini gösteriyor.

O türün adına bu algoritmada 'Tur' dedik. Onun yerine, çoğu C++ programcısının böyle tek türlü şablonlar için kullandığı 'T' harfini veya bambaşka bir adı da kullanabilirdik. Örneğin:

template <class T>
T toplam /* ... */

'toplam' algoritması bu programda 'main' içinden hem int hem de double türleri için kullanılıyor. Yani 'toplam' algoritması, her iki kullanıma göre sanki iki kere yazılmış gibi derleniyor. Sonuçta, algoritmayı bir kere yazmış ama iki değişik tür için kullanmış oluyoruz.

Derleyicinin algoritmayı hangi tür için çağırdığımızı anlamasına 'şablon parametresi çıkarımı' (template argument deduction) denir. Ben burada bu karmaşık düzeneği açıklamaya çalışmayacağım. Tek yapacağım, derleyicinin bu örnekte hangi çıkarımları yaptığını göstermek olacak: derleyici buradaki iki kullanıma bakarak 'Tur'ün 'int' ve 'double' türleri yerine kullanılması gerektiğini anlıyor.

Bu algoritmanın şimdiye kadar yazdığım hem C hem de C++ hallerinde daha önceden de belirttiğim bir eksiklik var: sayıların toplamını tutan 'sonuc' değişkeninin türü, toplamları alınan sayıların türleriyle aynı.

Eğer sonucu tutan değişkenin türünü de bağımsız olarak belirlemek istersek, şablonumuzu iki tür alacak şekilde değiştirmemiz gerekir. Hem sayıların türünü, hem de sonucun türünü belirtmemiz gerekir.

Şablon parametresi çıkarımı düzeneğinden bu yazıda olabildiğince uzak durabilmek için bir hile yapacak ve 'toplam' algoritmasına üçüncü bir parametre ekleyeceğim:

template <class SonucTuru, class SayiTuru>
SonucTuru toplam(SayiTuru const * bas,
                 SayiTuru const * son,
                 SonucTuru sonuc)
{
    for ( ; bas != son; ++bas)
    {
        sonuc += *bas;
    }
    
    return sonuc;
}

Algoritmanın yeni halinde toplamı sıfırdan başlatmak yerine, bize verilen 'sonuc'tan başlatacağız. Sanki algoritmanın kullanım alanını genişletir gibi görünse de, bunu biraz da derleyiciye toplamın dönüş türünü kolayca bildirmek için yapıyorum.

Yeni algoritmayı örneğin şöyle kullanabiliriz:

#include <iostream>

using std::cout;

template <class SonucTuru, class SayiTuru>
SonucTuru toplam(SayiTuru const * bas,
                 SayiTuru const * son,
                 SonucTuru sonuc)
{
    for ( ; bas != son; ++bas)
    {
        sonuc += *bas;
    }
    
    return sonuc;
}

#define OGE_SAYISI(x) (sizeof(x) / sizeof(*(x)))

int main()
{
    int const tamsayilar[] = { 7, 12, 30, 42 };

    double const onToplam = 0.123;

    cout << toplam(tamsayilar,
                   tamsayilar + OGE_SAYISI(tamsayilar),
                   onToplam) << '\n';
}

Algoritmamızın bu son hali, C++'ın standart kütüphanesinin <numeric> başlığında tanımlanan 'accumulate'e çok benzemiş oldu. Şimdi de aynı programı 'accumulate'i kullanarak yazalım:

#include <iostream>
#include <numeric>

using namespace std;

#define OGE_SAYISI(x) (sizeof(x) / sizeof(*(x)))

int main()
{
    int const tamsayilar[] = { 7, 12, 30, 42 };

    double onToplam = 0.123;

    cout << accumulate(tamsayilar,
                       tamsayilar + OGE_SAYISI(tamsayilar),
                       onToplam) << '\n';
}

C++'ın şablonlarının küçük bir uygulaması olarak kullandığımız toplama almak gibi basit bir algoritmanın bile bir miktar tasarım gerektirdiğini de görmüş olduk.

En sonunda da, yazdığımız algoritmanın standart kütüphanede zaten bulunduğunu keşfettik. Standart kütüphanedeki algoritmalar ve veri yapıları usta programcılar tarafından yazılıp denenmişlerdir. Standart kütüphaneyi kullanmak çoğu durumda yapılacak en akıllıca şeydir. Standart kütüphanenin uygun olmadığı nadir durumlarda algoritmalarımızı ve veri yapılarımızı kendimiz yazabiliriz.

Bundan sonraki yazımda standart kütüphanedeki topluluk veri yapılarından (containers) vector'ü tanıtacağım.


Ali Çehreli - acehreli@yahoo.com