C ++ 'da make_shared ve normal shared_ptr arasındaki fark


276
std::shared_ptr<Object> p1 = std::make_shared<Object>("foo");
std::shared_ptr<Object> p2(new Object("foo"));

Birçok google ve stackoverflow yayınları bu konuda, ama neden make_shareddoğrudan kullanmaktan daha verimli olduğunu anlayamıyorum shared_ptr.

Birisi bana nasıl yaratılmış olduğunu ve her ikisi tarafından yapılan işlemleri adım adım açıklayabilir miyim böylece nasıl make_sharedverimli olduğunu anlayabiliyorum . Referans için yukarıda bir örnek verdim.


4
Daha verimli değil. Kullanmanın nedeni istisna güvenliği içindir.
Yuushi

Bazı makaleler, bazı inşaat yüklerinden kaçınıyor, lütfen bunun hakkında daha fazla bilgi verebilir misiniz?
Anup Buchke

16
@Yuushi: İstisna güvenliği kullanmak için iyi bir nedendir, ancak daha verimlidir.
Mike Seymour

3
32:15, eğer yardımcı olursa, yukarıda bağlantı verdiğim videoda başladığı yerdir.
chris

4
Küçük kod stili avantajı: make_sharedyazma auto p1(std::make_shared<A>())ve p1 kullanarak doğru tipe sahip olacaksınız.
Ivan Vergiliev

Yanıtlar:


333

Fark, std::make_sharedbir yığın ayırma std::shared_ptrgerçekleştirirken , yapıcı çağırmak iki gerçekleştirir.

Yığın ayırmaları nerede olur?

std::shared_ptr iki kuruluşu yönetir:

  • kontrol bloğu (ref sayımları, tür silinmiş silme vb. gibi meta verileri depolar)
  • yönetilen nesne

std::make_sharedhem kontrol bloğu hem de veri için gerekli alan için tek bir yığın ayırma muhasebesi gerçekleştirir. Diğer durumda, new Obj("foo")yönetilen veriler için bir yığın ayırmayı çağırır ve std::shared_ptryapıcı kontrol bloğu için başka bir yığın gerçekleştirir.

Daha fazla bilgi için cppreference adresindeki uygulama notlarına bakın .

Güncelleme I: İstisna-Güvenlik

NOT (2019/08/30) : C ++ 17'den bu yana, işlev bağımsız değişkenlerinin değerlendirme sırasındaki değişiklikler nedeniyle bu bir sorun değildir. Özellikle, bir işleve ait her bağımsız değişkenin, diğer bağımsız değişkenlerin değerlendirilmesinden önce tam olarak yürütülmesi gerekir.

OP olayların istisnai güvenlik tarafını merak ediyor gibi göründüğünden cevabımı güncelledim.

Bu örneği ele alalım,

void F(const std::shared_ptr<Lhs> &lhs, const std::shared_ptr<Rhs> &rhs) { /* ... */ }

F(std::shared_ptr<Lhs>(new Lhs("foo")),
  std::shared_ptr<Rhs>(new Rhs("bar")));

C ++ alt ifadelerin rasgele değerlendirilmesine izin verdiğinden, olası bir sıralama şöyledir:

  1. new Lhs("foo"))
  2. new Rhs("bar"))
  3. std::shared_ptr<Lhs>
  4. std::shared_ptr<Rhs>

Şimdi, 2. adımda bir istisna aldığımızı varsayın (örneğin, bellek dışı istisna, Rhskurucu bir istisna attı). Daha sonra 1. adımda ayrılan belleği kaybediyoruz, çünkü hiçbir şeyin onu temizleme şansı olmayacaktı. Buradaki sorunun özü, ham işaretçinin std::shared_ptrderhal kurucuya geçmediğidir .

Bunu düzeltmenin bir yolu, bunları ayrı satırlarda yapmaktır, böylece bu arbiter sıralama gerçekleşemez.

auto lhs = std::shared_ptr<Lhs>(new Lhs("foo"));
auto rhs = std::shared_ptr<Rhs>(new Rhs("bar"));
F(lhs, rhs);

Bunu çözmenin tercih edilen yolu elbette kullanmaktır std::make_shared.

F(std::make_shared<Lhs>("foo"), std::make_shared<Rhs>("bar"));

Güncelleme II: Dezavantajı std::make_shared

Casey'nin yorumundan alıntı :

Yalnızca tek bir ayırma olduğundan, kontrol bloğu artık kullanılmayana kadar sınıfa ait bellek yer değiştirilemez. A weak_ptrkontrol bloğunu süresiz olarak hayatta tutabilir.

Neden weak_ptrs örnekleri kontrol bloğunu canlı tutuyor?

S'nin weak_ptryönetilen nesnenin hala geçerli olup olmadığını belirlemesinin bir yolu olmalıdır (örn. İçin lock). Bunu shared_ptr, kontrol bloğunda saklanan yönetilen nesneye sahip olan s sayısını kontrol ederek yaparlar . Sonuç, kontrol bloklarının shared_ptrsayım ve weak_ptrsayımın her ikisi 0'a ulaşana kadar hayatta kalmasıdır .

Sayfasına geri dön std::make_shared

Yana std::make_sharedkontrol bloğu ve yönetilen nesne için tek yığın ayırma yapan, kontrol bloğu için bellek ve bağımsız olarak yönetilen nesne serbest bir yolu yoktur. Biz kontrol bloğunu ve hiçbir kalmayıncaya kadar olur yönetilen nesne, hem özgür kadar beklemek zorundayız shared_ptrler veya weak_ptryaşıyor.

Bunun yerine, kontrol bloğu ve yönetilen nesne newve shared_ptryapıcı için iki yığın ayırma gerçekleştirdiğimizi varsayalım . Daha sonra shared_ptrcanlı olmadığında yönetilen nesnenin hafızasını (belki daha erken) boşaltırız ve canlı olmadığında kontrol bloğunun hafızasını (belki daha sonra) weak_ptrboşaltırız.


53
Köşedeki küçük kasanın dezavantajından da bahsetmek iyi bir fikirdir make_shared: sadece bir tahsis olduğundan, kontrol bloğu artık kullanılmayana kadar sivri hafızasının yeri değiştirilemez. A weak_ptrkontrol bloğunu süresiz olarak hayatta tutabilir.
Casey

14
Bir başka, daha üslup, nokta: Eğer kullanırsanız make_sharedve make_uniquesürekli olarak, ham işaretçiler sahibi olmazsınız ve her newkokusunu bir kod kokusu olarak ele alabilirsiniz .
Philipp

6
Yalnızca bir tane varsa shared_ptrve hiç değilse , örneği weak_ptrçağırmak kontrol bloğunu siler. Ancak bu ne olursa olsun ya da kullanılıp kullanılmadığı. Kullanımı bir fark yaratır, çünkü yönetilen nesne için ayrılan belleğin ömrünü uzatabilir . Ne zaman sayımı 0 vurur, yönetilen nesne için yıkıcı olursa olsun çağrılan , ancak eğer onun hafızası sadece yapılabilir azat edilmiş değil kullandı. Umarım bu daha net olur. reset()shared_ptrmake_sharedmake_sharedshared_ptrmake_sharedmake_shared
14'te mpark

4
Ayrıca, make_shared'in kontrol bloğunun daha küçük bir işaretçi olmasına izin veren "Nerede Yaşadığınızı Biliyoruz" optimizasyonundan faydalanabileceğini belirtmek gerekir. (Ayrıntılar için, Stephan T. Lavavej'in yaklaşık 12. dakikada GN2012 sunumuna bakın .) Make_shared böylece sadece bir ayırmayı önlemekle kalmaz, aynı zamanda daha az toplam bellek ayırır.
14:31

1
@HannaKhalil: Bu belki de aradığın bölge mi ...? melpon.org/wandbox/permlink/b5EpsiSxDeEz8lGH
mpark

26

Paylaşılan işaretçi hem nesnenin kendisini hem de referans sayısını ve diğer temizlik verilerini içeren küçük bir nesneyi yönetir. make_sharedbunların her ikisini birden tutmak için tek bir bellek bloğu tahsis edebilir; bir işaretçiden önceden atanmış bir nesneye paylaşılan bir işaretçi oluşturmak, referans sayısını saklamak için ikinci bir blok tahsis etmelidir.

Bu verimliliğin yanı sıra, kullanmak , daha iyi istisna güvenliği make_sharedsağlamak için uğraşmanıza gerek yok newve ham işaretçiler anlamına gelir - nesneyi tahsis ettikten sonra ancak akıllı işaretçiye atamadan önce bir istisna atma olasılığı yoktur.


2
İlk noktanızı doğru anladım. İstisna güvenliği ile ilgili ikinci noktada ayrıntılı bir şekilde bağlantı kurabilir misiniz?
Anup Buchke

22

İki olasılığın farklı olduğu başka bir durum daha önce bahsedilenlerin üstünde: kamuya açık olmayan bir kurucu (korumalı veya özel) çağırmanız gerekiyorsa, make_shared buna erişemeyebilir, ancak yeni işlerdeki varyasyon iyi çalışır .

class A
{
public:

    A(): val(0){}

    std::shared_ptr<A> createNext(){ return std::make_shared<A>(val+1); }
    // Invalid because make_shared needs to call A(int) **internally**

    std::shared_ptr<A> createNext(){ return std::shared_ptr<A>(new A(val+1)); }
    // Works fine because A(int) is called explicitly

private:

    int val;

    A(int v): val(v){}
};

Bu sorunla karşılaştım ve kullanmaya karar verdim new, aksi takdirde kullanardım make_shared. İşte bununla ilgili bir soru: stackoverflow.com/questions/8147027/… .
jigglypuff

6

Shared_ptr tarafından kontrol edilen nesne üzerinde özel bellek hizalaması gerekiyorsa, make_shared'e güvenemezsiniz, ancak bunu kullanmamanın tek iyi nedeni olduğunu düşünüyorum.


2
Make_shared öğesinin uygun olmadığı ikinci bir durum, özel bir silme belirtmek istediğiniz zamandır.
14:22

5

Std :: make_shared ile ilgili bir sorun görüyorum, özel / korumalı kurucuları desteklemiyor


3

Shared_ptr: İki yığın ayırma gerçekleştirir

  1. Kontrol bloğu (referans sayısı)
  2. Yönetilen nesne

Make_shared: Yalnızca bir yığın ayırma gerçekleştirir

  1. Kontrol bloğu ve nesne verileri.

0

Tahsis için harcanan verimlilik ve endişe zamanı hakkında, aşağıdaki basit testi yaptım, bu iki yolla (birer birer) birçok örnek oluşturdum:

for (int k = 0 ; k < 30000000; ++k)
{
    // took more time than using new
    std::shared_ptr<int> foo = std::make_shared<int> (10);

    // was faster than using make_shared
    std::shared_ptr<int> foo2 = std::shared_ptr<int>(new int(10));
}

Mesele şu ki, make_shared kullanmak yeni kullanmaya kıyasla iki kat zaman aldı. Yani, new kullanarak make_shared kullanan biri yerine iki yığın ayırma vardır. Belki bu aptalca bir test ama make_shared kullanmanın yeni kullanmaktan daha fazla zaman aldığını göstermiyor mu? Tabii ki, sadece kullanılan zamandan bahsediyorum.


4
Bu test biraz anlamsız. Test, optimizasyonlar açık olarak sürüm yapılandırmasında yapıldı mı? Ayrıca tüm öğeleriniz hemen serbest bırakılır, bu yüzden gerçekçi değildir.
Phil1970

0

Bay mpark'ın cevabının istisna güvenlik kısmı hala geçerli bir endişe kaynağı. böyle bir shared_ptr oluştururken: shared_ptr <T> (yeni T), yeni T başarılı olabilirken, shared_ptr'in kontrol bloğu tahsisi başarısız olabilir. bu senaryoda, yeni tahsis edilen T sızıntı yapar, çünkü shared_ptr'in yerinde oluşturulduğunu bilmesinin bir yolu yoktur ve onu silmek güvenlidir. yoksa bir şey mi kaçırıyorum? İşlev parametresi değerlendirme konusundaki daha katı kuralların burada hiçbir şekilde yardımcı olduğunu sanmıyorum ...

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.