Kaynak Edinimi ile Başlatma (RAII) nedir?


Yanıtlar:


374

İnanılmaz derecede güçlü bir konsept için gerçekten korkunç bir isim ve belki de C ++ geliştiricilerinin diğer dillere geçtiklerinde kaçırdığı 1 numaralı şeyden biri. Henüz kavranmamış gibi görünse de, bu kavramı Scope-Bound Kaynak Yönetimi olarak yeniden adlandırmaya çalışmak için biraz hareket oldu .

'Kaynak' dediğimizde sadece bellek demek değildir - dosya tutamakları, ağ yuvaları, veritabanı tutamakları, GDI nesneleri olabilir ... Kısacası, sınırlı bir kaynağa sahip olduğumuz şeyler ve bu yüzden yapabilmemiz gerekir kullanımlarını kontrol eder. 'Kapsama bağlı' yönü, nesnenin ömrünün bir değişkenin kapsamına bağlı olduğu anlamına gelir, bu nedenle değişken kapsam dışına çıktığında, yıkıcı kaynağı serbest bırakır. Bunun çok kullanışlı bir özelliği, daha fazla istisna güvenliği sağlamasıdır. Örneğin, bunu karşılaştırın:

RawResourceHandle* handle=createNewResource();
handle->performInvalidOperation();  // Oops, throws exception
...
deleteResource(handle); // oh dear, never gets called so the resource leaks

RAII one ile

class ManagedResourceHandle {
public:
   ManagedResourceHandle(RawResourceHandle* rawHandle_) : rawHandle(rawHandle_) {};
   ~ManagedResourceHandle() {delete rawHandle; }
   ... // omitted operator*, etc
private:
   RawResourceHandle* rawHandle;
};

ManagedResourceHandle handle(createNewResource());
handle->performInvalidOperation();

Bu son durumda, istisna atıldığında ve yığın çözüldüğünde, yerel değişkenler yok edilir, bu da kaynağımızın temizlenmesini ve sızmamasını sağlar.


2
@the_mandrill: ideone.com/1Jjzuc bu programı denedim. Ama yıkıcı bir çağrı yok. Tomdalling.com/blog/software-design/…, C ++ 'nın bir istisna atılsa bile yığındaki nesnelerin yok edicisinin çağrılmasını garanti ettiğini söylüyor. Peki, neden yıkıcı burada idam etmedi? Kaynağım sızdırılmış mı yoksa asla serbest bırakılmayacak mı veya serbest bırakılmayacak mı?
Destructor

8
Bir istisna atılır, ancak onu yakalamazsınız, bu nedenle uygulama sona erer. Bir deneme {} catch () {} ile sararsanız
the_mandrill

2
Scope-BoundDepolama sınıfı belirleyicilerinin kapsamla birlikte bir kuruluşun depolama süresini belirlediğinden, burada en iyi ad seçiminin olup olmadığından emin değilim . Kapsamına daraltmak belki de yararlı bir basitleştirmedir, ancak% 100 kesin değildir
SebNag

125

Bu bir programlama deyimidir.

  • bir kaynağı bir sınıfa kapsüllemek (yapıcı genellikle - ancak zorunlu olarak ** değil) kaynağı satın alır ve yıkıcısı her zaman serbest bırakır)
  • kaynağı sınıfın yerel bir örneği aracılığıyla kullanma *
  • nesne kapsam dışına çıktığında kaynak otomatik olarak serbest bırakılır

Bu, kaynak kullanımdayken ne olursa olsun, sonunda serbest bırakılacağını garanti eder (normal geri dönüş, içerdiği nesnenin imhası veya atılmış bir istisna nedeniyle).

C ++ 'da yaygın olarak kullanılan iyi bir uygulamadır, çünkü kaynaklarla başa çıkmanın güvenli bir yolu olmasının yanı sıra, hata işleme kodunu ana işlevsellik ile karıştırmanız gerekmediğinden kodunuzu daha temiz hale getirir.

* Güncelleme: "local", yerel bir değişken veya sınıfın statik olmayan bir üye değişkeni anlamına gelebilir. İkinci durumda üye değişkeni, sahip nesnesiyle başlatılır ve yok edilir.

** Güncelleme2: @sbi'nin işaret ettiği gibi, kaynak - genellikle yapıcı içinde tahsis edilmiş olmasına rağmen - dışarıda tahsis edilebilir ve parametre olarak aktarılabilir.


1
AFAIK, kısaltma, nesnenin yerel (yığın) bir değişken üzerinde olması gerektiği anlamına gelmez. Başka bir nesnenin üye değişkeni olabilir, bu nedenle 'holding' nesnesi yok edildiğinde üye nesne de yok edilir ve kaynak serbest bırakılır. Aslında, kısaltma özellikle kaynağı başlatmak ve serbest bırakmak için hiçbir yöntem open()/ close()yöntem olmadığını, sadece yapıcıyı ve yıkıcı olduğunu, bu yüzden kaynağın 'tutulması', nesnenin ömrüdür, içerik (yığın) veya açıkça (dinamik ayırma) tarafından işlenir
Javier

1
Aslında hiçbir şey kaynağın yapıcıda edinilmesi gerektiğini söylemiyor. Dosya akışları, başka bir kapsayıcı dizeleri bunu yapar, ancak kaynak genellikle akıllı işaretçilerde olduğu gibi kurucuya da aktarılabilir . Sizinki en çok oylanan cevap olduğundan, bunu düzeltmek isteyebilirsiniz.
sbi

Bir kısaltma değil, bir kısaltmadır. IIRC çoğu kişi bunu "ar ey ay ay" olarak telaffuz eder, dolayısıyla DARPA gibi bir kısaltma için hecelenmek yerine DARPA olarak telaffuz edilir. Ayrıca, RAII'nin sadece bir deyimden ziyade bir paradigma olduğunu söyleyebilirim.
dtech

@Peter Torok: Bu programı ideone.com/1Jjzuc'u denedim . Ama yıkıcı bir çağrı yok. Tomdalling.com/blog/software-design/... bir istisnası atılır bile, C ++ garanti yığın nesnelerinin yıkıcı adlı olacağını söylüyor. Peki, neden yıkıcı burada idam etmedi? Kaynağım sızdırılmış mı yoksa asla serbest bırakılmayacak mı veya serbest bırakılmayacak mı?
Destructor

50

"RAII", "Kaynak Edinimi Başlatmadır" anlamına gelir ve aslında kaynak edinimi (ve bir nesnenin başlatılması) değil, kaynağı serbest bırakması (bir nesnenin imhası yoluyla) serbest bırakılması nedeniyle oldukça yanlış bir isimdir. ).
Ama RAII sahip olduğumuz isim ve yapışıyor.

Özünde, deyim, yerel, otomatik nesnelerdeki kaynakları (bellek parçaları, açık dosyalar, kilitsiz muteksler, ad-it-it) sarmalayan ve nesne yok edildiğinde kaynağı serbest bırakan nesnenin yıkıcısına sahip ait olduğu kapsamın sonu:

{
  raii obj(acquire_resource());
  // ...
} // obj's dtor will call release_resource()

Tabii ki, nesneler her zaman yerel, otomatik nesneler değildir. Onlar da bir sınıfın üyesi olabilirler:

class something {
private:
  raii obj_;  // will live and die with instances of the class
  // ... 
};

Bu tür nesneler belleği yönetiyorsa, genellikle "akıllı işaretçiler" olarak adlandırılırlar.

Bunun birçok varyasyonu var. Örneğin, ilk kod parçacıklarında birisi kopyalamak isterse ne olacağı sorusu ortaya çıkar obj. En kolay çıkış, kopyalamaya izin vermemek olacaktır. std::unique_ptr<>bir sonraki C ++ standardının öne çıkardığı gibi standart kütüphanenin parçası olacak akıllı bir işaretçi bunu yapar.
Başka bir akıllı işaretçi, std::shared_ptrsahip olduğu kaynağın (dinamik olarak ayrılmış bir nesne) "paylaşılan sahipliğini" içerir. Yani, serbestçe kopyalanabilir ve tüm kopyalar aynı nesneyi ifade eder. Akıllı işaretçi, aynı nesneye kaç kopya atıfta bulunduğunu izler ve sonuncusu yok edildiğinde onu siler.
Üçüncü bir varyantstd::auto_ptr bir tür hareket semantiği uygular: Bir nesneye yalnızca bir işaretçi aittir ve bir nesneyi kopyalamaya çalışmak nesnenin sahipliğini kopyalama işleminin hedefine aktarmaya neden olur (sözdizimi korsanlığı yoluyla).


4
std::auto_ptreski sürümü std::unique_ptr. std::auto_ptrC ++ 98'de mümkün olduğunca simüle edilmiş hareket std::unique_ptrsemantiği, C ++ 11'in yeni hareket semantiğini kullanır. Yeni sınıf, C ++ 11'in taşıma semantiği daha açık ( std::movegeçici olanlar dışında gerektirir ), çünkü const olmayan öğeden herhangi bir kopya için varsayılan olarak oluşturuldu std::auto_ptr.
Jan Hudec

@JiahaoCai: Yıllar önce (Usenet'te) Stroustrup'un kendisi böyle söyledi.
sbi

21

Bir nesnenin ömrü kapsamı tarafından belirlenir. Bununla birlikte, bazen yaratıldığı kapsamdan bağımsız olarak yaşayan bir nesne yaratmamız gerekir veya faydalıdır. C ++ 'da, operatör newböyle bir nesne oluşturmak için kullanılır. Ve nesneyi yok etmek için operatör deletekullanılabilir. Operatör tarafından oluşturulan nesneler newdinamik olarak ayrılır, yani dinamik bellekte ( yığın veya serbest depolama olarak da adlandırılır) ayrılır . Böylece, tarafından oluşturulan bir nesne, newaçıkça kullanılarak yok edilene kadar var olmaya devam edecektir delete.

Kullanırken oluşabilecek bazı hatalar newve deleteşunlardır:

  • Sızan nesne (veya bellek): newbir nesneyi ayırmak ve nesneyi unutmak için deletekullanılır.
  • Erken silme (veya sarkan referans ): başka bir işaretçiyi bir nesneye, nesneye tutup diğer işaretçiyi deletekullanın.
  • Çift silme : deletebir nesneye iki kez denenir.

Genellikle, kapsamlandırılmış değişkenler tercih edilir. Bununla birlikte, RAII alternatif olarak kullanılabilir newve deletecanlı, bağımsız bir şekilde kapsamının bir nesne yapmak için. Böyle bir teknik, işaretçiyi öbek üzerinde tahsis edilen nesneye götürüp bir tutamaç / yönetici nesnesine yerleştirmekten oluşur . İkincisi, nesneyi yok etmeye özen gösterecek bir yıkıcıya sahiptir. Bu, nesnenin kendisine erişmek isteyen herhangi bir işlev için kullanılabilir olmasını ve açık nesneye ihtiyaç duyulmadan, tanıtıcı nesnenin ömrü sona erdiğinde nesnenin yok edilmesini garanti eder .

RAII kullanan C ++ standart kitaplığından örnekler std::stringve std::vector.

Şu kod parçasını düşünün:

void fn(const std::string& str)
{
    std::vector<char> vec;
    for (auto c : str)
        vec.push_back(c);
    // do something
}

bir vektör oluşturduğunuzda ve öğeleri ona aktardığınızda, bu tür öğeleri ayırmayı ve yeniden ayırmayı umursamazsınız. Vektör newöbek üzerindeki öğeleri için yer ayırmak ve bu alanı boşaltmak için kullanır delete. Bir vektörün kullanıcısı olarak uygulama ayrıntılarını önemsemezsiniz ve vektörün sızdırmamasına güvenirsiniz. Bu durumda, vektör, elemanlarının tutamak nesnesidir .

Standart kütüphanesinden diğer örnekleri kullanımı RAII olduğu std::shared_ptr, std::unique_ptrve std::lock_guard.

Bu tekniğin bir diğer adı da Kapsam Kapsamlı Kaynak Yönetiminin kısaltması olan SBRM'dir .


1
"SBRM" benim için çok daha mantıklı. Bu soruya geldim çünkü RAII'yi anladığımı düşündüm ama adı beni fırlatıp attı, bunun yerine "Kapsam-Bağlı Kaynak Yönetimi" olarak tanımlandığını duyunca, konsepti gerçekten anladığımı hemen anladım.
JShorthouse

Bunun neden sorunun cevabı olarak işaretlenmediğinden emin değilim. Bu çok kapsamlı ve iyi yazılmış bir cevap, teşekkürler @elmiomar
Abdelrahman Shoman

13

Ortaya Çıkan Tasarım Desenleri ile C ++ Programlama kitabı RAII'yi şöyle tarif eder:

  1. Tüm kaynakları edinme
  2. Kaynakları kullanma
  3. Kaynakları serbest bırakma

Nerede

  • Kaynaklar sınıf olarak uygulanır ve tüm işaretçilerin etrafında sınıf sarmalayıcıları bulunur (onları akıllı işaretçiler haline getirir).

  • Kaynaklar kurucularını çağırarak elde edilir ve yıkıcılarını çağırarak dolaylı olarak (edinme sırasının tersine) serbest bırakılır.


1
@Brandin Okuyumu, okurların adil kullanımı oluşturan şeylerin telif hakkı yasasının gri alanını tartışmak yerine önemli olan içeriğe odaklanmaları için düzenledim.
Dennis

7

Bir RAII sınıfının üç bölümü vardır:

  1. Kaynak yıkıcıya bırakıldı
  2. Sınıf örnekleri yığınlara ayrılmıştır
  3. Kaynak yapıcıda alınır. Bu bölüm isteğe bağlıdır, ancak yaygındır.

RAII, "Kaynak Edinimi başlatmadır" anlamına gelir. RAII'nin "kaynak edinimi" bölümü, daha sonra sona erdirilmesi gereken bir şeye başladığınız yerdir, örneğin:

  1. Dosya açma
  2. Biraz bellek ayırma
  3. Bir kilit alma

"Başlatma" bölümü, edinimin bir sınıfın yapıcısının içinde gerçekleştiği anlamına gelir.

https://www.tomdalling.com/blog/software-design/resource-acquisition-is-initialisation-raii-explained/


5

Manuel bellek yönetimi, programcıların derleyicinin icadından bu yana kaçınmanın yollarını icat ettiği bir kabustur. Çöp toplayıcılarla dil programlamak hayatı kolaylaştırır, ancak performans pahasına. Bu makalede - Çöp Toplayıcıyı ortadan kaldırmak: RAII Yolu , Toptal mühendisi Peter Goodspeed-Niklaus bize çöp toplayıcılarının tarihine bir göz atıyor ve sahiplik ve borçlanma kavramlarının güvenlik garantilerinden ödün vermeden çöp toplayıcılarını nasıl ortadan kaldırabileceğini açıklıyor.

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.