kopyala takas et idiyomu (copy & swap idiom)

Bu yazımızda C++’ın kopyala ve takas et idiyomunu ele alacağız. Bu idiyom, kaynak kullanan bir sınıfın atama operatör işlevinin yazılmasında sağlam hata güvencesi  (strong exception guarantee) oluşturmak amacıyla kullanılıyor. İdiyomu iyi anlayabilmek için öncelikle sağlam hata güvencesi ne demek, bunu hatırlamamız gerekiyor:

İşlevler hata işleme güvenliği (exception safety) açısından verdikleri güvencelere göre 4 ayrı kategoriye ayrılıyor:

1) Başarı güvencesi (no fail guarantee)
İşlev işini gerçekleştireceği güvencesini verir. Yani işlev içinden ya hiç hata nesnesi gönderilmeyecek ya da gönderilen hata nesnelerinin tümü yakalanıp gereken yapılacak ve işlev, hata nesneni gönderilmemiş gibi, başarılı olacak.

2) Sağlam güvence(strong guarantee)
İşlev ya başarılı olma ya da başarısızlık durumunda sınıf nesnesinin durumunda hiçbir değişiklik yapmama güvencesi verir. Yani sınıf nesnesi işlevden önce hangi durumdaysa işlevin çağrılmasından sonra da aynı durumda kalacak.

3) Temel güvence(basic guarantee)
İşlev, sınıf nesnesinin hiçbir kaynağını sızdırmama ve sınıf nesnesinin veri elemanlarının değerleri değişse de bu değerlerin geçerli sınırlar içinde kalacağı ve işlevin çağrılmasından önce saptanan ön koşulları (invariants) koruyacağı  güvencesini verir. Yani nesne hata gönderilmesinden önceki işlemlerle değiştirilmiş olabilir ama bu nesenin yeniden kullanımına engel bir durum oluşturmaz.

4) Güvencesiz (no guarantee)
İşlev  hata güvenliği açısından hiçbir güvence vermez.

İdiyomu basit bir sınıfı kullanarak kod üzerinde adım adım geliştirmemiz konunun çok daha iyi anlaşılmasını sağlayacak. Bir dinamik dizi sınıfımız olsun. İdiyoma odaklanabilmek için sınıfımızı mümkün olduğunca basit tutuyor ve bizi ilgilendirmeyen sınıf öğelerini kodda göstermiyoruz:

Yukarıdaki kodda yer alan DynamicArray isimli sınıfa alıcı gözüyle bir bakalım. Sınıf için yalnızca int parametreli bir kurucu işlev, kopyalayan kurucu işlev ve sonlandırıcı işlevi yazdık. İdiyomumuz başta söylediğimiz gibi henüz yazmadığımız kopyalayan atama operatörü işlevinde kullanılacak.
Sınıfımızın iki tane static olmayan veri öğesi var. size_t türünden m_size isimli öğe elde edilecek dinamik dizinin boyutunu, m_pdata isimli gösterici öğemiz de dinamik dizinin yer alacağı dinamik bellek bloğunun adresini tutacaklar.
Kurucu işlevimizin size_t türden parametresi varsayılan arguman olarak 0 değerini alıyor. Böylece bu işlev varsayılan kurucu işlev olarak kullanıldığında henüz elemanı olmayan bir dinamik dizi oluşturacak. İstenmeyen otomatik tür dönüşümlerinin engellenmesi için kurucu işlevi explicit yaptık. Kurucu işlevimiz öğe ilk değer verme listesiyle (member initializer list)  m_size öğesine dinamik dizinin boyutunu, m_pdata öğesine  de new [] işleciyle elde edilen dinamik dizinin adresini  yerleştiriyor.

Şimdi de sınıfın kopyalayan kurucu işlevine bakalım: Kopyalayan kurucu işlevimiz, yine öğe ilk değer verme listesiyle, kopyalamada kullanılacak, parametre r referansına bağlanan diğer sınıf nesnesinin boyutu kadar bir dinamik diziyi new[] işleciyle elde ediyor. İşlevin ana bloğu içinde STL‘in copy algoritmasını kullanarak, hayata gelen nesnenin dinamik dizisine diğer dizinin öğelerini kopyalıyoruz.  Dinamik dizimizin hiçbir öğesinin olmaması durumunda her iki kurucu işlevde de m_size öğesi 0, m_pdata öğesi de nullptr değerini alıyor.

Şimdi sınıfımızın kopyalayan atama operatörü işlevini aklımıza ilk gelen biçimde yazalım:

Koyalayan atama operatör işlevimiz önce nesnenin kendi kendine atanıp atanmadığını test ediyor. Kendi kendine atama söz konusu değilse önce kendi dinamik dizisini delete işleciyle geri veriyor. Sonra derin kopyalama (deep copy) yapıyor. Görünürde her şey olması gerektiği gibi. Ama duruma bir de hata işleme güvenliği açısından bakalım.
Bir hata nesnesi gönderildiğinde sağlam hata güvencesinin söz konusu olması için, nesnemizin ilk durumunu koruması gerekiyor. Yani ya işlem başarılı olmalı ya da nesneniz işlemden önceki durumunu aynı şekilde korumalı. Peki new[] işleci başarısız olur da std::bad_alloc sınıfı türünden bir hata nesnesi gönderirse ne olacak?  Dinamik bellek alanı geri verildi ve nesnemizin m_size öğesi değiştirildi. Yani nesnemiz artık işlem öncesindeki nesnemiz değil. Peki sağlam hata güvencesi ele etmek için ne yapmalıyız? Dinamik bellek alanımızı delete etmeden  önce yeni bellek alanımızı elde edelim ve ondan sonra veri öğelerimizin değerlerini değiştirelim:

Şimdi artık new[] işleci bir hata nesnesi gönderse de nesnemiz işlem öncesindeki değerini koruyacak.  Yeni kodumuz aslında nesnenin kendi kendine atanması durumunda da bir hataya neden olmayacak, çünkü new[] işleciyle elde edilen dinamik dizinin adresini bir başka yerel gösterici değişkene atıyoruz. if deyimi ile kendi kendine atama durumu sınanmasaydı da bir hata söz konusu olmazdı. Nesnenin kendi kendine atanması durumunda gereksiz yere bir bellek alanı elde etmiş ve gereksiz yere bir kopyalama yapmış olurduk ama kendi kendine atamanın çok nadir gerçekleşeceğini düşünürsek bu bizi çok da rahatsız etmemeli:

Kopyalayan atama operatörü işlevimizin ilk üç deyimi kopyalayan kurucu işlevimizin kodu ile aynı. Ortak kodu tek bir yere toplamak için atama operatör işlevinin kodu içinde, bir yerel nesneyi kopyalayan kurucu işlev ile oluşturabilir ve bu yerel nesneyle *this nesnesini takas edebiliriz:

Önce friend olarak tanımlanan global swap işlevine bakalım. Bu işlevin kodu içinde standart kütüphanenin swap işleviyle veri elemanlarını takas ediyoruz. Atama operatör işlevimiz içinde önce DynamicArray sınıfı türünden bir nesneyi parametre olan DynamicArray nesnesi ile hayata getiriyoruz. Burada oluşturulan yerel cp nesnesi için kopyalayan kurucu işlev çağrılacak. Ardından cp yerel nesnesini *this nesnesi ile takas ediyoruz. Burada swap işlemini sınıfa friend olan swap işlevi yürütecek. cp nesnesi için blok sonunda sonlandırıcı işlev çağrıldığında dinamik bellek alanı delete edilmiş olacak.

Bundan daha iyisini yapabiliriz. swap işleminde kullanılacak nesneyi derleyici oluşturamaz mı?

Atama operatör işlevimizin parametre değişkeni const DynamicArray & yerine doğrudan DynamicArray türünden yapıldı. Böylecce bu işlev çağrıldığında parametre değişkeni x için kopyalayan kurucu işlev çağrılacak. İşlevimizin kodu içinde gerçekleştirilen swap işlevinde işlevin parametre değişkeni kullanıldı. Bu değişiklik derleyicinin eniyileme olanaklarını genişlettiği gibi taşıma semantiğine de destek verecek.

Necati Ergin

C ve Sistem Programcıları Derneğinde eğitmen olarak çalışıyor.

Bunlar da ilginizi çekebilir

Bir Cevap Yazın

E-posta hesabınız yayımlanmayacak. Gerekli alanlar * ile işaretlenmişlerdir

Kod Eklemek İçin Okuyun