Büyük bir listeyi tahrip etmek yığımı taşacak mı?


12

Aşağıdaki tek bağlantılı liste uygulamasını düşünün:

struct node {
    std::unique_ptr<node> next;
    ComplicatedDestructorClass data;
}

Şimdi diyelim std::unique_ptr<node> headki daha sonra kapsam dışı olan ve yıkıcısının çağrılmasına neden olan bir örneği kullanmayı bırakıyorum.

Bu, yığınımı yeterince büyük listeler için patlatacak mı? Adil derleyici (inline oldukça karmaşık optimizasyon yapacak varsaymak mı unique_ptr'içine s yıkıcı nodebunu yaparsam beri takip (çok zor aldığı halde, s' kuyruk Özyinelemeyi kullanın) datayıkıcı obfuscate ediyorum next, 'ın zor hale getirmekte derleyicinin olası yeniden sıralama ve kuyruk arama fırsatını fark etmesi için):

struct node {
    std::shared_ptr<node> next;
    ComplicatedDestructorClass data;
}

Bir dataşekilde bir işaretçisi varsa node, kuyruk özyineleme için bile imkansız olabilir (tabii ki bu tür kapsülleme ihlallerinden kaçınmaya çalışmalıyız).

Genel olarak, başka türlü bu listeyi nasıl yok edecek? Listede dolaşamayız ve "geçerli" düğümü silemeyiz, çünkü paylaşılan işaretçide bir release! Tek yol, benim için gerçekten kötü kokulu özel bir silme ile.


1
İkinci durumda bahsedilen kapsülleme ihlali olmasa bile, değerinde olan şey, gcc -O3bir kuyruk özyinelemesini optimize edemedi (karmaşık bir örnekte).
VF1

1
Burada cevabınız var: Derleyici özyinelemeyi optimize edemezse yığınızı patlatabilir.
Bart van Ingen Schenau

@BartvanIngenSchenau Sanırım bu sorunun başka bir örneği . Akıllı işaretçi temizliğini sevdiğim için de gerçek bir utanç.
VF1

Yanıtlar:


7

Evet, derleyici node'yıkıcıya ve shared_ptr ' yıkıcısına sadece bir kuyruk çağrısı optimizasyonu uygulamadığı sürece, bu, sonunda yığınızı havaya uçuracaktır . İkincisi, standart kütüphane uygulamasına son derece bağımlıdır. Örneğin, Microsoft'un shared_ptrSTL'si bunu asla yapmayacaktır, çünkü ilk önce sivrisinekinin referans sayısını azaltır (muhtemelen nesneyi yok eder) ve daha sonra kontrol bloğunun referans sayısını (zayıf referans sayısı) azaltır. Yani iç yıkıcı kuyruk çağrısı değildir. Aynı zamanda sanal bir çağrıdır , bu da optimize edilme olasılığını azaltır.

Tipik listeler, bir düğümü diğerine sahip olmadan, ancak tüm düğümlere sahip olan ve yıkıcıdaki her şeyi silmek için bir döngü kullanan bir sorunla bu sorunu çözer.


Evet, shared_ptrsonunda olanlar için özel bir silme ile "tipik" liste silme algoritmasını uyguladım . İplik güvenliğine ihtiyaç duyduğum için işaretçilerden tamamen kurtulamıyorum.
VF1

Paylaşılan işaretçi "sayaç" nesnenin de sanal bir yıkıcı olacağını bilmiyordum, her zaman sadece güçlü refs + zayıf refs + deleter tutan bir POD olduğunu varsaydım ...
VF1

@ VF1 İşaretçilerin istediğiniz iplik güvenliğini sağladığından emin misiniz?
Sebastian Redl

Evet std::atomic_*, onlar için aşırı yüklenmelerin asıl noktası bu , değil mi?
VF1

Evet, ama bu da başaramayacağınız bir şey değil std::atomic<node*>, daha ucuz.
Sebastian Redl

5

Geç cevap ama kimse bunu sağlamadığından ... Ben aynı sorunla karşılaştım ve özel bir yıkıcı kullanarak çözdüm:

virtual ~node () throw () {
    while (next) {
        next = std::move(next->next);
    }
}

Gerçekten bir listeniz varsa , yani her düğümden önce bir düğüm gelir ve en fazla bir takipçisi varsa ve listilkiniz için bir işaretçiyseniz node, yukarıdakilerin çalışması gerekir.

Bazı bulanık yapılarınız varsa (örneğin asiklik grafik), aşağıdakileri kullanabilirsiniz:

virtual ~node () throw () {
    while (next && next.use_count() < 2) {
        next = std::move(next->next);
    }
}

Fikir şu ki:

next = std::move(next->next);

Eski paylaşılan işaretçi nextyok edildi ( use_countşimdi olduğu için 0) ve aşağıdakilere işaret ediyorsunuz. Bu, varsayılan yıkıcıyla tamamen aynıdır, ancak yinelemeli olarak yinelemeli olarak yapar ve böylece yığın taşmasını önler.


İlginç fikir. OP'nin iş parçacığı güvenliği gereksinimini karşıladığından emin değil, ancak konuya başka açılardan yaklaşmanın kesinlikle iyi bir yolu.
Jules

Her ederken koşul ile, bir kez en fazla değerlendirilecektir, gerçek listesinde - sen hareket operatörü aşırı sürece, bu yaklaşım aslında bir şey kazandırır emin nasıl değilim next = std::move(next->next)çağıran next->~node()yinelemeli.
VF1

1
@ VF1 Bu next->next, çünkü işaret ettiği değer nextyok edilmeden (hareket atama operatörü tarafından) geçersiz kılındığı için özyinelemeyi "durdurur". Aslında bu kodu ve bu işi (test kullanmak g++, clangve msvc), ama şimdi bunu söylemek, ben bu standardın (taşınan işaretçi sivri eski nesnenin imha önce geçersiz olması ile tanımlanır emin değilim hedef işaretçiyle).
Holt

@ VF1 Güncellemesi: Standarda göre operator=(std::shared_ptr&& r), eşdeğerdir std::shared_ptr(std::move(r)).swap(*this). Yine de, standarttan hareket std::shared_ptr(std::shared_ptr&& r)ediciyi rboşaltır, dolayısıyla çağrıdan önce rboştur ( r.get() == nullptr) swap. Benim durumumda, bu next->nextişaret eski nesnenin next( swapçağrı ile) yok edilmesinden önce boştur .
Holt

1
@ VF1 Kodunuz aynı değil - çağrısı façık next, değil next->nextve next->nextboş olduğu için hemen durur.
Holt

1

Dürüst olmak gerekirse, herhangi bir C ++ derleyicisinin akıllı işaretçi ayırma algoritmasına aşina değilim, ancak bunu yapan basit, özyinelemesiz bir algoritma hayal edebiliyorum. Bunu düşün:

  • Yeniden konumlandırmayı bekleyen akıllı işaretçiler kuyruğunuz var.
  • İlk işaretçiyi alan ve onu yeniden konumlandıran ve kuyruk boşalana kadar bunu tekrarlayan bir işleviniz var.
  • Akıllı bir işaretçinin yeniden konumlandırılması gerekiyorsa, sıraya itilir ve yukarıdaki işlev çağrılır.

Bu nedenle, yığının taşması için bir şans olmaz ve özyinelemeli bir algoritmayı optimize etmek çok daha kolaydır.

Bunun "neredeyse sıfır maliyetli akıllı işaretçiler" felsefesine uyup uymadığından emin değilim.

Açıkladığınız şeyin yığın taşmasına neden olmayacağını tahmin ediyorum, ancak yanlış olduğunu kanıtlamak için akıllı bir deney oluşturmayı deneyebilirsiniz.

GÜNCELLEME

Bu, daha önce yazdıklarımın yanlış olduğunu kanıtlıyor:

#include <iostream>
#include <memory>

using namespace std;

class Node;

Node *last;
long i;

class Node
{
public:
   unique_ptr<Node> next;
   ~Node()
   {
     last->next.reset(new Node);
     last = last->next.get();
     cout << i++ << endl;
   }
};

void ignite()
{
    Node n;
    n.next.reset(new Node);
    last = n.next.get();
}

int main()
{
    i = 0;
    ignite();
    return 0;
}

Bu program sonsuza dek bir düğüm zinciri oluşturur ve bir yapı zinciri oluşturur. Yığın taşmasına neden olur.


1
Ah, devam eden bir stil mi demek istiyorsun? Etkili bir şekilde, tanımladığınız şey budur. Ancak, daha önce sadece eski bir listeyi yeniden konumlandırmak için yığın üzerinde başka bir liste oluşturmaktan daha akıllı işaretçileri feda ederdim .
VF1

Ben hatalıydım. Cevabımı buna göre değiştirdim.
Gábor Angyal
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.