İşaretçileri, hafızayı std::unique_ptr
sınıf şablonunun bir örneği tarafından yönetilen nesnelere geçirmenin farklı uygulanabilir modlarını belirtmeye çalışayım ; aynı zamanda eski std::auto_ptr
sınıf şablonu (ki benzersiz işaretçinin yaptığı tüm kullanımlara izin verdiğine inanıyorum, ancak ek olarak, değerlerin beklendiği yerlerde, çağrılmak zorunda kalmadan değiştirilebilen değerlerin kabul edileceğine inanıyorum std::move
) ve bir ölçüde de geçerlidir std::shared_ptr
.
Tartışmanın somut bir örneği olarak aşağıdaki basit liste türünü ele alacağım.
struct node;
typedef std::unique_ptr<node> list;
struct node { int entry; list next; }
Bu listenin örnekleri (parçaları başka örneklerle paylaşmasına veya dairesel olmasına izin verilemez) tamamen ilkini elinde bulunduran kişiye aittir. list
işaretçiyi . İstemci kodu depoladığı listenin hiçbir zaman boş olmayacağını biliyorsa, ilk önce node
a yerine doğrudan depolamayı da seçebilir list
. Herhangi bir yıkıcı node
tanımlanmasına gerek yoktur: alanları için yıkıcılar otomatik olarak çağrıldığından, ilk işaretçi veya düğümün ömrü sona erdiğinde tüm liste akıllı işaretçi yıkıcı tarafından yinelemeli olarak silinecektir.
Bu özyinelemeli tür, düz verilere akıllı bir işaretçi durumunda daha az görünür olan bazı durumları tartışma fırsatı verir. Ayrıca işlevlerin kendileri de zaman zaman (yinelemeli olarak) bir istemci kodu örneği sağlarlar. İçin typedef list
elbette önyargılıdır unique_ptr
, ancak tanım kullanılmak üzere auto_ptr
veya shared_ptr
bunun yerine aşağıda söylenenlere çok fazla ihtiyaç duyulmaksızın değiştirilebilir (özellikle yıkıcılar yazmaya gerek kalmadan istisna güvenliği ile ilgilidir).
Akıllı işaretçileri geçirme modları
Mod 0: Akıllı işaretçi yerine işaretçi veya başvuru bağımsız değişkenini iletme
İşleviniz sahiplikle ilgili değilse, bu tercih edilen yöntemdir: hiç akıllı bir işaretçi almayın. Bu durumda, işlevin işaret edilen nesneye kimin sahip olduğu veya sahipliğin yönetildiği ile endişelenmenize gerek yoktur , bu nedenle ham bir işaretçiyi geçmek hem tamamen güvenli hem de en esnek formdur, çünkü sahiplik ne olursa olsun bir müşteri her zaman ham bir işaretçi oluşturun (get
yöntemi veya operatörün adresinden &
).
Örneğin, bu listenin uzunluğunu hesaplama işlevi bir list
argüman değil, ham bir işaretçi olmalıdır:
size_t length(const node* p)
{ size_t l=0; for ( ; p!=nullptr; p=p->next.get()) ++l; return l; }
Değişken içeren bir istemci, list head
bu işlevi şu şekilde çağırabilir length(head.get())
; bunun yerine,node n
boş olmayan bir listeyi temsil eden bir çağırabilir length(&n)
.
İşaretçinin boş olmadığı garanti edilirse (listeler boş olabileceğinden burada durum böyle değildir), işaretçi yerine referans geçmeyi tercih edebilir. Olmayan bir işaretçi / referans olabilirconst
Fonksiyonun herhangi bir düğüm eklemeden veya çıkarmadan düğümün içeriğini güncellemesi gerekip gerekmediğine (ikincisi sahipliği içerecektir).
0 modu kategorisine giren ilginç bir durum, listenin (derin) bir kopyasını oluşturmaktır; bunu yapan bir işlev elbette oluşturduğu kopyanın sahipliğini devretmekle birlikte, kopyaladığı listenin sahipliğiyle ilgilenmez. Böylece şu şekilde tanımlanabilir:
list copy(const node* p)
{ return list( p==nullptr ? nullptr : new node{p->entry,copy(p->next.get())} ); }
Bunun için (hiç özyinelemeli aramanın sonucunu derler neden olarak soru için hem bu kod yararları yakından bakmak, copy
bir hareket kurucusundaki rvalue referans tartışmaya Başlatıcı liste bağlar içinde unique_ptr<node>
aka list
, başlatılıyor zaman next
alanını üretilen node
) ve özel durum korumalı (özyinelemeli tahsis sürecinin bellek biterse ve bazı çağrı sırasında bir eğer neden olarak soru için new
atar std::bad_alloc
, sonra o zaman kısmen inşa listesine bir işaretçi türü geçici anonim tutulurlist
başlatıcı listesi için oluşturulur ve yıkıcısı bu kısmi listeyi temizler). Bu arada bir (Başlangıçta yaptığı gibi) ikinci değiştirmek için günaha karşı gerektiği nullptr
tarafındanp
ve sonuçta bu noktada null olduğu bilinir: null olduğu bilinse bile (ham) bir işaretçiden sabite bir akıllı işaretçi yapılamaz.
Mod 1: Akıllı işaretçiyi değere göre geçirin
Akıllı işaretçi değerini bağımsız değişken olarak alan bir işlev hemen işaret edilen nesneye sahip olur: arayanın tuttuğu akıllı işaretçi (adlandırılmış bir değişken veya anonim geçici olarak) işlev girişindeki bağımsız değişken değerine ve arayanın imleç geçersiz hale geldi (geçici bir durumda kopya kaldırılmış olabilir, ancak her durumda arayan, işaretlenen nesneye erişimi kaybetmiştir). Bu mod çağrısını nakit olarak aramak istiyorum : arayan, çağrılan hizmet için ön ödeme yapar ve çağrıdan sonra sahiplik hakkında hiçbir yanılsama yaşayamaz. Bunu açıklığa kavuşturmak için, dil kuralları arayanın argümanı içine almasını gerektirirstd::move
akıllı işaretçi bir değişkende tutulursa (teknik olarak, bağımsız değişken bir değerse); bu durumda (ancak aşağıdaki mod 3 için değil), bu işlev adından da anlaşılacağı gibi, değeri değişkenten geçici hale getirip değişkeni null bırakarak yapar.
Aranan işlevin koşulsuz olarak sivri uçlu nesnenin sahipliğini aldığı (pilfers), bu mod, sahip olduğu std::unique_ptr
veya std::auto_ptr
işaretçi ile birlikte geçmesi için iyi bir yoldur ve bu da bellek sızıntısı riskini önler. Bununla birlikte, aşağıdaki mod 3'ün mod 1'de tercih edilmeyeceği (çok az) sadece çok az durum olduğunu düşünüyorum. Bu nedenle, bu modun hiçbir kullanım örneğini vermeyeceğim. (Ancak reversed
aşağıdaki mod 3 örneğine bakın, burada mod 1'in de en azından yapacağı belirtilmiştir.) İşlev yalnızca bu işaretçiden daha fazla argüman alırsa, moddan kaçınmanın teknik bir nedeni de olabilir. 1 (std::unique_ptr
veyastd::auto_ptr
): bir işaretçi değişkeni iletilirken gerçek bir hareket işlemi gerçekleştiğindenp
ifade ile diğer argümanları değerlendirirken faydalı bir değer tutar (değerlendirme sırası belirtilmemiş), bu da ince hatalara yol açabilir; tersine, mod 3 kullanıldığında , işlev çağrısından önce herhangi bir hareket yapılmamasını sağlar , böylece diğer argümanlar bir değere güvenli bir şekilde erişebilir .std::move(p)
, olduğu varsayılamazp
p
p
std::shared_ptr
Bununla birlikte kullanıldığında , bu mod, tek bir işlev tanımı ile, arayanın işlev tarafından kullanılacak yeni bir paylaşım kopyası oluştururken işaretçinin paylaşım kopyasını kendisi için tutup tutmayacağını seçmesine izin vermesi ilginçtir (bu, bir değer değeri bağımsız değişken sağlanır; çağrıda kullanılan paylaşılan işaretçiler için kopya oluşturucu referans sayısını artırır) veya işleve bir tanesini tutmadan veya referans sayısına dokunmadan işaretçinin bir kopyasını vermek için (bu, muhtemelen bir rvalue bağımsız değişkeni sağlandığında gerçekleşir çağrısına sarılmış bir lvalue std::move
). Örneğin
void f(std::shared_ptr<X> x) // call by shared cash
{ container.insert(std::move(x)); } // store shared pointer in container
void client()
{ std::shared_ptr<X> p = std::make_shared<X>(args);
f(p); // lvalue argument; store pointer in container but keep a copy
f(std::make_shared<X>(args)); // prvalue argument; fresh pointer is just stored away
f(std::move(p)); // xvalue argument; p is transferred to container and left null
}
Aynı şey ayrı ayrı tanımlanarak void f(const std::shared_ptr<X>& x)
(lvalue durumu için) ve void f(std::shared_ptr<X>&& x)
(rvalue durumu için), işlev gövdelerinin yalnızca ilk sürümün kopya semantiğini (kullanırken kopya oluşturma / atama kullanarak x
) ancak ikinci sürüm taşıma semantiğini kullanmasıyla farklılık göstermesi ile elde edilebilir. ( std::move(x)
bunun yerine örnek kodda olduğu gibi yazma ). Bu nedenle, paylaşılan işaretçiler için, mod 1 bazı kod çoğaltmalarını önlemek için yararlı olabilir.
Mod 2: Akıllı bir işaretçiyi (değiştirilebilir) değer referansıyla geçirin
Burada fonksiyon sadece akıllı işaretçiye değiştirilebilir bir referansa sahip olmayı gerektirir, ancak onunla ne yapacağına dair hiçbir belirti vermez. Bu yöntemi kart ile aramak istiyorum : arayan kredi kartı numarası vererek ödeme sağlar. Referans olabilir sivri-nesneye ancak zorunda değildir sahipliğini almak için kullanılabilir. Bu mod, işlevin istenen etkisinin argüman değişkeninde yararlı bir değer bırakmayı içerebileceği gerçeğine karşılık gelen değiştirilebilir bir değer bağımsız değişkeni sağlamayı gerektirir. Bu tür bir işleve geçmek istediği rvalue ifadesine sahip bir arayan, çağrıyı yapabilmek için adlandırılmış bir değişkende saklamak zorunda kalacaktır, çünkü dil yalnızca bir sabite dolaylı dönüştürme sağlarbir değerden referans değeri (geçici olarak atıfta bulunarak). (Ele alınan zıt durumdan farklı olarak , akıllı işaretçi türüyle std::move
bir oyuncudan yayın Y&&
yapmak mümkün değildir; yine de bu dönüşüm gerçekten istenirse basit bir şablon işlevi ile elde edilebilir; bkz. Https://stackoverflow.com/a/24868376 / 1436796 ). Aranan işlevin koşulsuz olarak nesnenin sahipliğini almayı amaçladığı ve argümandan çaldığı durumlarda, bir değer değeri argümanı sağlama yükümlülüğü yanlış sinyal verir: değişkenin çağrıdan sonra yararlı bir değeri olmayacaktır. Bu nedenle, fonksiyonumuz içinde özdeş olasılıklar sunan ancak arayanlardan bir değer sağlamasını isteyen mod 3, bu kullanım için tercih edilmelidir.Y&
Y
Bununla birlikte, mod 2 için geçerli bir kullanım durumu vardır, yani işaretçiyi değiştirebilecek işlevler veya sahiplik içeren bir şekilde işaret edilen nesne . Örneğin, bir düğüme önek ekleyen bir işlev list
, bu tür kullanımlara bir örnek sağlar:
void prepend (int x, list& l) { l = list( new node{ x, std::move(l)} ); }
Açıkça burada, arayanları kullanmaya zorlamak istenmeyen bir durum olacaktır std::move
, çünkü akıllı işaretçileri hala çağrıdan sonra iyi tanımlanmış ve boş olmayan bir listeye sahiptir, ancak öncekinden farklı bir liste.
Yine prepend
, boş hafıza eksikliği nedeniyle çağrı başarısız olursa ne olduğunu gözlemlemek ilginçtir . Sonra new
çağrı atacak std::bad_alloc
; bu noktada, node
tahsis edilemediğinden, atanan rvalue referansının (mod 3) std::move(l)
henüz tahsis edilemediği next
alanın inşası için yapılacağı için henüz çalınamayacağı kesindir node
. Böylece l
hata atıldığında orijinal akıllı işaretçi hala orijinal listeyi tutar; bu liste ya akıllı işaretçi yıkıcı tarafından düzgün bir şekilde imha edilecek ya da l
yeterince erken bir catch
fıkra sayesinde hayatta kalması durumunda orijinal listeyi koruyacaktır.
Bu yapıcı bir örnekti; bir göz kırpma ile bu sorunun bir de, eğer varsa, belli bir değeri içeren birinci düğüm kaldırma daha yıkıcı örnek verebiliriz:
void remove_first(int x, list& l)
{ list* p = &l;
while ((*p).get()!=nullptr and (*p)->entry!=x)
p = &(*p)->next;
if ((*p).get()!=nullptr)
(*p).reset((*p)->next.release()); // or equivalent: *p = std::move((*p)->next);
}
Yine burada doğruluk oldukça incedir. Özellikle, son ifadede, (*p)->next
kaldırılacak düğümün içinde tutulan işaretçinin bağlantısı kaldırılır (ile release
, işaretçiyi döndürür, ancak orijinali null yapar), bu düğümü (örtülü olarak) yok etmeden önce reset
(tarafından tutulan eski değeri yok ettiğinde p
) bir ve sadece bir düğüm o anda yok edilir. ( Yorumda adı geçen alternatif formda, bu zamanlama, std::unique_ptr
örneğin taşıma-atama operatörünün uygulanmasının list
içlerine bırakılacaktır; Standart, bu operatörün "20.7.1.2.3; 2 çağıran reset(u.release())
zamanlama burada da güvenli olmalıdır nereden,".)
O Not prepend
ve remove_first
yerel saklamak müşteriler tarafından çağrılamaz node
bir zaman boş olmayan liste için değişken ve haklı olarak çok uygulamaları gibi durumlarda için yapamadı iş verilir beri.
Mod 3: Akıllı bir işaretçiyi (değiştirilebilir) değer referansıyla geçirin
İşaretçinin sahipliğini alırken kullanmak için tercih edilen mod budur. Bu yöntem çağrıyı çekle çağırmak istiyorum : arayan nakit çekiyormuş gibi çek bırakma sahipliğini kabul etmelidir, ancak çek imzalanarak gerçek çekilme, çağrılan işlev aslında işaretçiyi çalıncaya kadar ertelenir (mod 2'yi kullanırken olduğu gibi) ). "Çekin imzalanması" somut olarak, arayanların std::move
bir değerse (bir değer ise, "sahiplikten vazgeçme" kısmı açıktır ve ayrı bir kod gerektirmezse) ( argümanı 1 modunda olduğu gibi) bir argümanı sarmaları gerektiği anlamına gelir .
Teknik olarak mod 3'ün tam olarak mod 2 gibi davrandığını, bu nedenle çağrılan işlevin sahiplik üstlenmesine gerek olmadığını unutmayın ; ancak (normal kullanımda) sahiplik transferi hakkında herhangi bir belirsizlik varsa, mod 2 moduna 3'ü kullanarak onlar arayanlar için bir sinyal örtülü olduğu böylece, mod 3'e tercih edilmelidir ısrar edeceğini edilir sahipliğini vazgeçerek. Sadece mod 1 argümanının geçmesinin gerçekten arayanlara zorla sahiplik kaybına işaret ettiğini doğrulayabilir. Ancak bir istemcinin çağrılan işlevin niyetleri hakkında herhangi bir şüphesi varsa, çağrılan işlevin özelliklerini bilmesi gerekir, bu da herhangi bir şüphe ortadan kaldırmalıdır.
list
Mod 3 argümanını kullanan tipimizi içeren tipik bir örnek bulmak şaşırtıcı derecede zordur . Bir listeyi b
başka bir listenin sonuna taşımak a
tipik bir örnektir; ancak a
(operasyonun sonucunu koruyan ve tutan) mod 2 kullanılarak daha iyi geçirilir:
void append (list& a, list&& b)
{ list* p=&a;
while ((*p).get()!=nullptr) // find end of list a
p=&(*p)->next;
*p = std::move(b); // attach b; the variable b relinquishes ownership here
}
Mod 3 bağımsız değişken geçişinin saf bir örneği, bir listeyi (ve sahipliğini) alan ve aynı düğümleri içeren bir listeyi ters sırada döndüren şudur.
list reversed (list&& l) noexcept // pilfering reversal of list
{ list p(l.release()); // move list into temporary for traversal
list result(nullptr);
while (p.get()!=nullptr)
{ // permute: result --> p->next --> p --> (cycle to result)
result.swap(p->next);
result.swap(p);
}
return result;
}
Bu işlev l = reversed(std::move(l));
, listeyi kendisine ters çevirmek için olduğu gibi çağrılabilir , ancak tersine çevrilmiş liste farklı şekilde kullanılabilir.
Burada argüman verimlilik için derhal yerel bir değişkene taşınır (biri parametreyi l
doğrudan yerinde kullanabilir p
, ancak daha sonra her seferinde erişmek ekstra bir dolaylı seviye içerir); dolayısıyla mod 1 argüman geçişi arasındaki fark minimumdur. Aslında, bu modu kullanarak, argüman doğrudan yerel değişken olarak işlev görebilir ve böylece ilk hareketten kaçınabilir; bu sadece referansla iletilen bir argüman sadece bir yerel değişkeni başlatmaya hizmet ediyorsa, bunun yerine değere göre de geçebilir ve parametreyi yerel değişken olarak kullanabilir.
Mod 3'ün kullanılması, mod 3'ü kullanarak akıllı işaretçilerin sahipliğini aktaran tüm sağlanan kütüphane işlevlerinin tanık olduğu gibi standart tarafından savunulduğu anlaşılmaktadır std::shared_ptr<T>(auto_ptr<T>&& p)
. (Kullanılan Yani yapıcı std::tr1
) bir değiştirilebilir almaya lvalue (tıpkı referans auto_ptr<T>&
kopya kurucu) ve bu nedenle bir ile denebilecek auto_ptr<T>
lvalue p
olduğu gibi std::shared_ptr<T> q(p)
bundan sonra, p
boş olarak sıfırlanmış. Bağımsız değişken geçerken mod 2'den 3'e değişiklik nedeniyle, bu eski kodun üzerine yeniden yazılması gerekir std::shared_ptr<T> q(std::move(p))
ve daha sonra çalışmaya devam eder. Komitenin burada mod 2'yi beğenmediğini anlıyorum, ancak tanımlayarak, mod 1'e geçme seçeneği vardıstd::shared_ptr<T>(auto_ptr<T> p)
bunun yerine, eski kodun değiştirilmeden çalışmasını sağlayabilirlerdi, çünkü (benzersiz işaretçilerden farklı olarak) otomatik işaretçiler sessizce bir değere silinebilir (işaretçi nesnesi işlemde null değerine sıfırlanır). Görünüşe göre komite, mod 1 yerine mod 3'ü savunmayı tercih etti , zaten kullanımdan kaldırılmış bir kullanım için bile mod 1'i kullanmak yerine mevcut kodu aktif olarak kırmayı seçtiler .
Mod 3 yerine mod 3 ne zaman tercih edilir
Mod 1 birçok durumda mükemmel bir şekilde kullanılabilir ve mülkiyetin aksi halde akıllı işaretçiyi reversed
yukarıdaki örnekte olduğu gibi yerel bir değişkene taşıma şeklini alacağı varsayıldığında mod 3'e göre tercih edilebilir . Bununla birlikte, daha genel bir durumda mod 3'ü tercih etmek için iki neden görebilirim:
Bir referansı geçmek, geçici bir işaret oluşturmaktan ve eski işaretçiyi birleştirmekten biraz daha etkilidir (nakit kullanımı biraz zahmetlidir); bazı senaryolarda işaretçi, gerçekte çalınmadan önce başka bir işleve birkaç kez değişmeden geçirilebilir. Bu tür geçişler genellikle yazmayı gerektirir std::move
(mod 2 kullanılmadığı sürece), ancak bunun sadece hiçbir şey yapmayan (özellikle de kayıt silme yok) bir döküm olduğunu unutmayın, bu nedenle sıfır maliyete sahiptir.
Herhangi bir şeyin fonksiyon çağrısının başlaması ile onun (ya da içerdiği bir çağrının) gerçekte sivri uçlu nesneyi başka bir veri yapısına taşıdığı nokta arasında bir istisna fırlatması düşünülebilirse (ve bu istisna zaten fonksiyonun içinde yakalanmamıştır) ), mod 1 kullanıldığında, akıllı işaretçi tarafından atıfta bulunulan nesne, bir catch
cümlenin istisnayı işleyebilmesi için imha edilir (çünkü fonksiyon parametresi yığın çözme sırasında tahrip edildiğinden), ancak mod 3 kullanılırken böyle olmaz. arayan, bu gibi durumlarda (istisna yakalayarak) nesnenin verilerini kurtarma seçeneğine sahiptir. Buradaki mod 1'in bellek sızıntısına neden olmadığını , ancak program için geri alınamayan veri kaybına neden olabileceğini ve bu da istenmeyen olabilir.
Akıllı işaretçiyi döndürme: her zaman değere göre
Akıllı bir işaretçiyi döndürmeyle ilgili bir kelimeyi sonuçlandırmak için , muhtemelen arayan tarafından kullanılmak üzere oluşturulmuş bir nesneyi işaret etmek. Bu gerçekten fonksiyonları içine işaretçileri geçirmeden ile kıyaslanabilir bir durum değildir, ancak şeyiyle ben böyle durumlarda ısrar etmek istiyorum her zaman değerleriyle dönmek (ve kullanmayan std::move
içinde return
deyimi). Hiç kimse muhtemelen yeni kurulmuş olan bir işaretçiye referans almak istemez .