Ders 04 / 14
RAII
Bir koruyucunun aldığı ve bıraktığı üyenin, koruyucu nesnenin kendisi değil içindeki bir parça olduğunu ölçer.
Yayın Güncelleme
Bu derste
- RAII'nin bir kaynağı yapıcıda alıp yıkıcıda bıraktığını tanımlar
- Bir koruyucunun dokunduğu nesnenin kendisi değil içindeki üye olduğunu ölçer
- Erken çıkış yollarında bile yıkıcının koştuğunu gösterir
İçindekiler
Önceki ders bir nesnenin ne zaman kurulduğunu ve yok olduğunu, adı olsun ya da olmasın, ölçtü. Bu ders o bilgiyi bir amaç için kullanır: bir kaynağı — burada basit bir sayaç kaynağı, gerçek programlarda bellek, dosya tanıtıcısı ya da kilit olabilir — güvenle serbest bırakmak. Ölçüm sınıfı bu kez parça: bir koruyucunun aldığı ve bıraktığı şey, koruyucunun kendisi değil, içindeki bir veri üyesidir.
RAII Tanımı
RAII (Resource Acquisition Is Initialization — kaynak edinimi ilk değerlemedir), bir
kaynağın bir nesnenin yapıcısında alınıp yıkıcısında bırakılmasını öngören bir tasarım
kalıbıdır. Kaynağı tutan nesne yaşadığı sürece kaynak da geçerlidir; nesne — adlandırılmış
ya da geçici, önceki derste görüldüğü gibi — herhangi bir yoldan yaşam süresinin sonuna
geldiğinde yıkıcı çalışır ve kaynağı bırakır. Bu, kaynağı elle bırakmayı (serbest_birak()
gibi bir işlevi her çağıran kodda tek tek çağırmayı) gereksiz kılar: kaynağı tutan nesne
hangi yoldan yok olursa olsun (işlevin sonuna gelme, erken return, istisna fırlatma),
yıkıcı her zaman çalışır.
RAII’nin karşıtı, kaynağı bir yapıcı/yıkıcı çiftine değil, çağıranın disiplinine bırakmaktır:
bir kaynak_ac() işlevi çağrılır, kullanılır, ve kodun her çıkış noktasında elle bir
kaynak_kapat() çağrılması beklenir. Bu yaklaşımın kırılganlığı, kod büyüdükçe ortaya çıkar
— bir işleve yeni bir erken return eklendiğinde, o return’den önce kaynak_kapat()
çağrısının da eklenmesi gerekir; eklenmezse kaynak sızar (leak — kaynağın hiç bırakılmadan
programın geri kalanında geçerli kalmaya devam etmesi). RAII bu riski, bırakma kararını
kaynağı tutan kodun değil, kaynağı tutan nesnenin türünün sorumluluğuna vererek ortadan
kaldırır: Koruyucu’yu kullanan kod hiçbir zaman bir bırakma çağrısı yazmaz, çünkü bu
çağrı zaten Kaynak::~Kaynak() içinde, bir kez, sınıfın kendisinde yazılıdır. Bu dersin ilk
bölümünde iki sınıf tanımlanır: kaynağı gerçekten tutan Kaynak, ve onu bir üye olarak saran
Koruyucu.
// kaynak.hpp — sayilarla temsil edilen basit bir kaynak
#ifndef KAYNAK_HPP
#define KAYNAK_HPP
#include <cstdio>
class Kaynak {
public:
explicit Kaynak(int kimlik) : kimlik_(kimlik) {
std::printf("kaynak alindi kimlik=%d\n", kimlik_);
}
~Kaynak() {
std::printf("kaynak birakildi kimlik=%d\n", kimlik_);
}
int kimlik() const { return kimlik_; }
private:
int kimlik_;
};
#endif
// koruyucu.hpp — kaynagi tutan uyeyi alan ve birakan koruyucu
#ifndef KORUYUCU_HPP
#define KORUYUCU_HPP
#include "kaynak.hpp"
class Koruyucu {
public:
explicit Koruyucu(int kimlik) : kaynak_(kimlik) {}
int kimlik() const { return kaynak_.kimlik(); }
private:
Kaynak kaynak_;
};
#endif
Koruyucunun Bıraktığı, Kendisi Değil İçindeki Üyedir
Koruyucu’nun kendi bir yapıcısı ve derleyicinin ürettiği bir yıkıcısı vardır; Koruyucu
kaynak_ üyesini kendi kurduğunda Kaynak yapıcısı çalışır, kendi yok olduğunda kaynak_
yıkılır ve Kaynak yıkıcısı çalışır. Buradaki soru: koruyucunun aldığı ve bıraktığı şey,
Koruyucu nesnesinin kendisi mi, yoksa içindeki kaynak_ üyesi mi?
// parca-olcumu.cpp — koruyucunun tuttugu kaynak koruyucunun kendisi mi ici mi
#include <cstdio>
#include "koruyucu.hpp"
int main() {
std::printf("blok basladi\n");
{
Koruyucu k(7);
std::printf("kimlik %d\n", k.kimlik());
}
std::printf("blok bitti\n");
return 0;
}
blok basladi kaynak alindi kimlik=7 kimlik 7 kaynak birakildi kimlik=7 blok bitti
SK8. Kaynak’ın yapıcısı Koruyucu k(7) satırında, Koruyucu’nun kendi yapıcı gövdesi
çalışmadan önce, üye ilk değer listesindeki kaynak_(kimlik) ile tetiklenir — kaynak alindi satırı bu yüzden blok basladi’dan hemen sonra, Koruyucu’nun kendi gövdesi hiçbir
şey basmadan önce görülür. Kaynağı gerçekte alan Kaynak nesnesidir, ve bu nesne
Koruyucu’nun kendisi değil, onun bir üyesidir — kaynak_, Koruyucu’nun bellek
aralığının içinde bir alt nesnedir (subobject), ayrı, bağımsız bir nesne değildir. Bunun
kanıtı sıralamadadır: k blok sonunda yok olurken önce Koruyucu’nun (derleyicinin
ürettiği, hiçbir şey basmayan) yıkıcısı çalışır, ardından Koruyucu’nun üyesi olduğu için
kaynak_’ın yıkıcısı çalışır — kaynak birakildi satırı bu yüzden blok bitti’den önce
görülür. Eğer kaynak_ Koruyucu’dan bağımsız, ayrı bir nesne olsaydı, onun yıkımı
Koruyucu’nun yıkımına bağlı olmazdı; oysa burada ikisi aynı anda, aynı kapsamın
kapanmasıyla tetiklenir — çünkü biri diğerinin parçasıdır.
Burada dikkat edilmesi gereken bir isim çakışması yoktur: Kaynak ile Koruyucu’nun ikisi
de kendi başlarına birer sınıftır, biri diğerini üye olarak barındırır. Koruyucu’nun
kendi yapıcısı explicit Koruyucu(int kimlik) : kaynak_(kimlik) {} gövdesizdir — tüm işi
üye ilk değer listesi yapar, kaynak_’ı kimlik ile kurar. Koruyucu’nun kendi bir
yıkıcısı yazılmamıştır; derleyici bir tane üretir, ve bu üretilen yıkıcı hiçbir şey
basmadan yalnız kaynak_’ın yıkıcısını tetikler — bu, önceki dersteki Ciftlik örneğinde
görülen aynı davranıştır: bir sınıf kendi yıkıcısını yazmasa bile, üyelerinin yıkıcıları
yine de çalışır.
Erken Çıkış Yollarında Yıkıcı Yine Çalışır
RAII’nin asıl değeri, kaynağı bırakan kodu tek bir yere (yıkıcıya) yazıp, o kodun her
çıkış yolunda çalışmasını derleyiciye bırakmaktır — işlevin normal sonu, erken return,
ya da bir istisna fırlatıldığında bile.
// erken-cikis.cpp — koruyucu erken return yolunda da birakiyor mu
#include <cstdio>
#include "koruyucu.hpp"
static void calistir(bool erkenCik) {
Koruyucu k(9);
std::printf("calistir basladi kimlik=%d\n", k.kimlik());
if (erkenCik) {
std::printf("erken cikiliyor\n");
return;
}
std::printf("normal bitiyor\n");
}
int main() {
calistir(true);
std::printf("main devam ediyor\n");
return 0;
}
kaynak alindi kimlik=9 calistir basladi kimlik=9 erken cikiliyor kaynak birakildi kimlik=9 main devam ediyor
SK9. calistir(true) if bloğu içindeki return’e ulaştığında, k’nin kapsamı
(işlev gövdesi) sona erer ve derleyici k’nin yıkıcısını return’den hemen önce
çalıştırır — kaynak birakildi satırı erken cikiliyor’dan sonra, ama main devam ediyor’dan önce görülür. Bu davranış if dalına bakılmaksızın aynıdır: calistir(false)
çağrılsaydı normal bitiyor satırından sonra yine aynı yıkıcı çalışırdı. Kaynağı bırakan kod
yalnız Kaynak::~Kaynak() içinde bir kez yazılıdır; calistir’in kendisi hiçbir bırakma
çağrısı yapmaz.
Bu güvence yalnız if/return gibi düzenli kontrol akışı için değil, C++’ın istisna
mekanizması için de geçerlidir: bir işlev gövdesinde bir istisna fırlatılırsa (throw),
yığıt o istisna bir catch bloğu tarafından yakalanana kadar çözülür
(yığıt çözülmesi) — bu çözülme sırasında, çözülen her kapsamdaki yerel nesnelerin
yıkıcıları, tıpkı normal bir return’de olduğu gibi sırayla çalıştırılır. calistir
içinde k’den sonra bir throw satırı olsaydı, k’nin yıkıcısı yine de istisna
calistir‘in dışına çıkmadan önce çalışırdı. Bu kurs istisna fırlatma ve yakalamayı
öğretmez — o başka bir konudur — ama RAII’nin gerçek gücü tam olarak buradan gelir: kaynak
bırakma kodu, kontrol akışının hangi yoldan çıktığını bilmek zorunda değildir, çünkü
yıkıcı her yoldan çalışır.
Kaynağı Referansla Tutmanın Riski
RAII’nin sağlamlığı, koruyucunun kaynağı değerle (bir üye olarak, kendi ömrüyle bağlı)
tutmasına dayanır. Bir koruyucu kaynağı referansla ya da işaretçiyle tutarsa, koruyucunun
kendi ömrü ile kaynağın gerçek sahibinin ömrü ayrışabilir — koruyucu hâlâ yaşarken kaynak
zaten yok olmuş olabilir. Koruyucu bu riski taşımaz çünkü kaynak_ bir referans ya da
işaretçi değil, doğrudan bir Kaynak değeridir; Koruyucu yaşadığı sürece kaynak_ de
yaşar, çünkü aynı nesnenin bir parçasıdır. Bir koruyucu kaynağı değerle değil de bir
işaretçiyle tutacak şekilde yanlış tasarlansaydı — Kaynak* kaynak_; gibi — koruyucunun
kopyalanması yalnız işaretçiyi kopyalardı, gerçek Kaynak nesnesini değil; iki koruyucu aynı
kaynağı işaret eder, biri yıkıcısında kaynağı bırakınca diğeri artık geçersiz bir kaynağa
işaret eder hâle gelirdi. Bu ders bu hatayı koşturmaz — yalnız düzyazıda anılır — çünkü
kaynağın iki kez bırakılması tanımsız davranıştır.
Bu paylaşım sorununu standart kütüphanenin hazır RAII sarmalayıcılarıyla çözmek K05’in konusudur; bu kurs kaynağı kendi yazdığı sınıflarla yönetir. M10/K02’nin (Rust) sahiplik sistemi ve M11/K02’nin kaynak yönetimi aynı fikrin — bir kaynağın tam olarak bir sahibi olması ve sahibin ömrü bittiğinde kaynağın otomatik bırakılması — farklı dillerdeki karşılıklarıdır; bu ders o karşılaştırmayı derinleştirmez.
Koruyucu’nun kendi tasarımı bu riskten nasıl kaçındığını daha ayrıntılı görmek
faydalıdır. kaynak_ bir Kaynak değeridir, bir Kaynak* ya da Kaynak& değildir;
bu yüzden Koruyucu nesnesinin bellek düzeninde kaynak_’ın kendisi (yalnız ona işaret
eden bir adres değil) yer alır. Bir Koruyucu kopyalandığında — bu kursun bir sonraki
dersinin konusu — kaynak_ de kopyalanır, çünkü o da Koruyucu‘nun bir parçasıdır; iki
Koruyucu iki ayrı Kaynak taşır, aynı kaynağı paylaşmazlar. Bir işaretçiyle tutulan bir
kaynakta bu doğru olmaz: işaretçinin kopyalanması yalnız adresi kopyalar, gösterilen nesneyi
değil, bu yüzden iki koruyucu aynı kaynağa işaret etmeye başlar. Değerle tutmanın bu
sonucu — kopyalamanın kaynağı da beraberinde kopyalaması — RAII’nin güvenilirliğinin bir
parçasıdır ve bir sonraki derste ayrıntılı ölçülür.
Bir kaynağı değerle tutmanın bir sınırı da vardır: kaynak, tutan nesnenin bellek düzeninde
yer aldığı için, kaynağın boyutu (ya da en azından kaynağı temsil eden tanıtıcının boyutu)
derleme zamanında bilinmelidir. Kaynak burada yalnız bir int taşıdığı için bu sorun
görünmez; gerçek bir kaynak — açık bir dosya, bir ağ bağlantısı — genellikle işletim
sisteminin verdiği sabit boyutlu bir tanıtıcı (handle, örneğin bir tamsayı ya da bir
işaretçi) ile temsil edilir, bu yüzden aynı değerle-tutma stratejisi gerçek kaynaklar için
de çalışır.
Çoklu Kaynak Tutan Bir Koruyucu
Bir koruyucunun tek bir kaynak yerine birden çok kaynak tutması da aynı kuralı izler: her
kaynak, koruyucunun bir başka üyesi olur, ve her üyenin kurulma/yıkılma sırası önceki
derste ölçülen bildirim sırası kuralına uyar. İki Kaynak üyesi olan bir CiftKoruyucu
tasarlansaydı, üyeler bildirim sırasıyla kurulur, ters sırada yıkılırdı — tıpkı Ciftlik
örneğindeki birinci_/ikinci_ gibi. Bu ders CiftKoruyucu‘yu koşturmaz, çünkü tek bir
kaynağın alınıp bırakılması RAII’nin çekirdek fikrini göstermek için yeterlidir; ama bu
genellemenin doğrudan önceki dersten geldiğini görmek önemlidir — RAII yeni bir dil kuralı
eklemez, yalnız yapıcı/yıkıcı sırasını bir amaç için kullanır.
Sınırlayıcı Ölçüm
Bu dersin ölçtüğü ifade parça sınıfındadır: kaynak_, Koruyucu’nun kendisi değil,
onun içindeki bir alt nesnedir, ve Koruyucu‘nun yaşam süresiyle aynı yaşam süresine
sahiptir. Ama RAII’nin güvencesi koruyucunun kaynağı değerle tutmasına bağlıdır — bir
koruyucu kaynağa yalnız bir referans tutuyorsa (kaynağın gerçek sahibi başka bir nesneyse),
koruyucu korunan nesneden uzun yaşamamalıdır; koruyucu hâlâ yaşarken korunan nesne yok
olursa, koruyucunun referansı sarkan hâle gelir ve koruyucunun yıkıcısı (varsa) geçersiz bir
nesneye erişmeye çalışabilir. Bu ders bu riski koşturmaz, çünkü koşturmak tanımsız davranışı
tetikler; Koruyucu burada kaynağı her zaman değerle tuttuğu için bu risk zaten yoktur.
Bu sınırın pratik bir sonucu, bir koruyucu tasarlarken “bu kaynağın gerçek sahibi kim?”
sorusunun her zaman açıkça yanıtlanması gerektiğidir. Koruyucu’da yanıt nettir: Koruyucu
kendisi sahiptir, çünkü kaynak_ onun bir değer üyesidir. Bir koruyucu bunun yerine
“başka bir yerde yaşayan bir kaynağı yalnız gözlemliyorum” demek isteseydi — örneğin bir
işlevin parametresinde alınan bir Kaynak&’i saklamak isteseydi — bu artık RAII değil,
yalnız bir referans tutmak olurdu, ve kaynağı bırakma sorumluluğu koruyucuda olmazdı. İki
tasarım da C++’ta geçerlidir, ama birbirine karıştırılmamalıdır: bir sınıfın yıkıcısı
yalnız kendi sahip olduğu kaynakları bırakmalıdır, ödünç aldığı bir kaynağı değil.
Bu ders boyunca ölçülen üç örnek — kurulma sırasının kanıtı, erken çıkışta yıkıcının
çalışması, kaynağın değerle tutulması — birlikte RAII’nin neden bir “en iyi uygulama”dan
fazlası olduğunu gösterir: bu, dilin yapıcı/yıkıcı ve bildirim sırası kurallarının doğrudan
bir sonucudur, ayrı bir mekanizma değildir. Bir sonraki dersin kopyalama sorusu da aynı
temelin üstüne kurulur — bir Koruyucu kopyalandığında kaynak_ de kopyalanacaktır, ama
bu kopyanın “derin” mi “sığ” mı olduğu, kaynağın türüne bağlı olarak farklı sonuçlar
doğurabilir.
Özet
- Bu dersin ölçtüğü ifade parça sınıfındadır:
kaynak_,Koruyucunesnesinin kendisi değil, onun bir alt nesnesidir. - RAII bir kaynağı yapıcıda alır, yıkıcıda bırakır; kaynağı elle bırakan kod gerekmez.
- Yıkıcı, kapsamın nasıl sona erdiğine bakılmaksızın çalışır — normal bitiş, erken
return, ya da istisna fırlatma, hepsinde aynıdır. - Kaynağı değerle tutan bir koruyucu, koruyucunun ömrüyle kaynağın ömrünü birleştirir; referansla tutan bir koruyucu bu güvenceyi kaybeder.
- Akıllı işaretçiler RAII’yi paylaşılan kaynaklara genişletir; bu kursun kapsamı dışındadır (K05).
Sonraki Adım
Bu ders bir kaynağın yapıcı/yıkıcı çiftiyle nasıl güvenle tutulduğunu ölçtü; bir sonraki ders bir nesne kopyalandığında bu güvencenin ne olduğunu sorar — kopyalayan yapıcının kendi kaynağını mı aldığını, yoksa orijinaliyle paylaştığını mı ölçer.
İlerlemeni kaydetmek ve not almak için Giriş yap
Notlarım
Not almak için giriş yapmalısın.