การทำลายรายการใหญ่จะล้นสแต็กของฉันหรือไม่


12

พิจารณาการใช้งานรายการที่เชื่อมโยงโดยลำพังดังต่อไปนี้:

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

ทีนี้สมมติว่าฉันหยุดใช้std::unique_ptr<node> headอินสแตนซ์ที่เกินขอบเขตแล้วทำให้ destructor ของมันถูกเรียก

สิ่งนี้จะทำให้สแตกของฉันสำหรับรายการขนาดใหญ่เพียงพอหรือไม่ มันยุติธรรมที่จะคิดว่าคอมไพเลอร์จะทำเพิ่มประสิทธิภาพซับซ้อนสวย (อินไลน์unique_ptr's destructor เข้าnode' s แล้วใช้ recursion หาง) ซึ่งได้รับยากมากถ้าผมทำต่อไปนี้ (ตั้งแต่datadestructor จะทำให้งงงวยnext's ทำให้มันยาก เพื่อให้คอมไพเลอร์สังเกตเห็นโอกาสการเรียงลำดับใหม่และโอกาสในการโทรหาง):

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

ถ้าdataอย่างใดมีตัวชี้ไปnodeแล้วก็อาจเป็นไปไม่ได้สำหรับการเรียกซ้ำหาง (แม้ว่าแน่นอนเราควรพยายามหลีกเลี่ยงการละเมิด encapsulation ดังกล่าว)

โดยทั่วไปแล้วเราจะทำลายรายการนี้ได้อย่างไร? เราไม่สามารถข้ามผ่านรายการและลบโหนด "ปัจจุบัน" เนื่องจากตัวชี้ที่ใช้ร่วมกันไม่มีrelease! วิธีเดียวคือกับตัวกำหนดเองซึ่งส่งกลิ่นให้ฉันจริงๆ


1
สำหรับสิ่งที่คุ้มค่าแม้ว่าจะไม่มีการละเมิดการห่อหุ้มที่กล่าวถึงในกรณีที่สอง แต่gcc -O3ก็ไม่สามารถปรับการเรียกซ้ำหางให้เหมาะสม (ในตัวอย่างที่ซับซ้อน)
VF1

1
ที่นั่นคุณมีคำตอบของคุณ: มันอาจทำให้สแต็คของคุณเกิดขึ้นหากคอมไพเลอร์ไม่สามารถปรับการเรียกซ้ำให้เหมาะสม
Bart van Ingen Schenau

@BartvanIngenSchenau ฉันเดาว่าเป็นอีกตัวอย่างหนึ่งของปัญหานี้ มันเป็นความอัปยศจริงเช่นกันเนื่องจากฉันชอบความสะอาดของตัวชี้อัจฉริยะ
VF1

คำตอบ:


7

ใช่สิ่งนี้จะทำให้สแตกของคุณในที่สุดเว้นแต่ว่าคอมไพเลอร์เพิ่งเกิดขึ้นเพื่อใช้การเพิ่มประสิทธิภาพการโทรหางกับตัวnodeทำลายและตัว shared_ptrทำลายของ สิ่งหลังขึ้นอยู่กับการใช้งานไลบรารีมาตรฐานอย่างมาก ตัวอย่างเช่น STL ของ Microsoft จะไม่ทำเช่นนั้นเพราะจะshared_ptrลดจำนวนการอ้างอิงของปวง (ก่อนที่จะทำลายวัตถุ) จากนั้นลดจำนวนการอ้างอิงของบล็อกควบคุม (จำนวนการอ้างอิงที่อ่อนแอ) ดังนั้นตัวทำลายภายในจึงไม่ใช่การเรียกหาง นอกจากนี้ยังเป็นสายเสมือนซึ่งทำให้มีโอกาสน้อยที่จะได้รับการปรับให้เหมาะสม

รายการทั่วไปจะแก้ไขปัญหานั้นได้โดยไม่มีโหนดตัวใดตัวหนึ่งเป็นเจ้าของตัวถัดไป แต่ด้วยการมีหนึ่งคอนเทนเนอร์ที่เป็นเจ้าของโหนดทั้งหมดและใช้วนรอบเพื่อลบทุกอย่างใน destructor


ใช่ฉันใช้อัลกอริทึมการลบรายการ "ทั่วไป" กับ deleter ที่กำหนดเองสำหรับผู้shared_ptrที่ในที่สุด ฉันไม่สามารถกำจัดพอยน์เตอร์ได้อย่างสมบูรณ์เนื่องจากฉันต้องการความปลอดภัยของเธรด
VF1

ฉันไม่ทราบว่าวัตถุ "counter" ของตัวชี้ที่ใช้ร่วมกันจะมี destructor เสมือนเช่นกันฉันคิดเสมอว่ามันเป็นเพียง POD ที่มีการอ้างอิงที่แข็งแกร่ง + refs ที่อ่อนแอ + deleter ...
VF1

@ VF1 คุณแน่ใจหรือไม่ว่าพอยน์เตอร์ให้ความปลอดภัยกับกระทู้ที่คุณต้องการ?
เซบาสเตียนเรดล

ใช่ - นั่นคือจุดรวมของการstd::atomic_*โอเวอร์โหลดสำหรับพวกเขาใช่ไหม?
VF1

ใช่ แต่นั่นคือสิ่งที่คุณไม่สามารถทำได้ด้วยstd::atomic<node*>เช่นกันและราคาถูกกว่า
เซบาสเตียนเรดล

5

คำตอบที่ล่าช้า แต่เนื่องจากไม่มีใครให้ ... ฉันพบปัญหาเดียวกันและแก้ไขโดยใช้ destructor แบบกำหนดเอง:

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

หากคุณจริงๆมีรายการคือทุกโหนดนำโดยหนึ่งโหนดและมีมากที่สุดคนหนึ่งลูกศิษย์และของคุณlistเป็นตัวชี้ไปที่แรกnodeข้างต้นควรจะทำงาน

หากคุณมีโครงสร้างที่คลุมเครือบางอย่าง (เช่นกราฟอะคริลิค) คุณสามารถใช้สิ่งต่อไปนี้:

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

แนวคิดก็คือเมื่อคุณทำ:

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

ตัวชี้ที่ใช้ร่วมกันแบบเก่าnextถูกทำลาย (เนื่องจากuse_countเป็นตอนนี้0) และคุณชี้ไปที่ต่อไปนี้ สิ่งนี้ทำแบบเดียวกันกับตัวทำลายเริ่มต้นยกเว้นว่าจะทำซ้ำ ๆ ซ้ำ ๆ และหลีกเลี่ยงการล้นสแต็ก


ความคิดที่น่าสนใจ ไม่แน่ใจว่าเป็นไปตามข้อกำหนดของ OP สำหรับความปลอดภัยของเธรด แต่เป็นวิธีที่ดีในการเข้าถึงปัญหาในด้านอื่น ๆ
จูลส์

ฉันจะไม่แน่ใจว่าวิธีการนี้บันทึกอะไรจริง ๆ ได้อย่างไร - ในรายการจริงแต่ละเงื่อนไขในขณะนั้นจะได้รับการประเมินมากที่สุดหนึ่งครั้งด้วยการnext = std::move(next->next)โทรnext->~node()ซ้ำ
VF1

1
@ VF1 ใช้งานได้เนื่องจากnext->nextไม่ถูกต้อง (โดยตัวดำเนินการกำหนดค่าการย้าย) ก่อนที่ค่าที่ชี้ไปnextจะถูกทำลายดังนั้นจึง "หยุด" การสอบถามซ้ำ ที่จริงผมใช้รหัสนี้และงานนี้ (ทดสอบกับg++, clangและmsvc) แต่ตอนนี้ที่คุณพูดว่าผมไม่แน่ใจว่านี้จะถูกกำหนดโดยมาตรฐาน (ความจริงที่ว่าตัวชี้ย้ายจะถูกยกเลิกก่อนการล่มสลายของวัตถุเก่าแหลม โดยตัวชี้เป้าหมาย)
โฮลต์

@ VF1 ปรับปรุง: ตามมาตรฐานเทียบเท่ากับoperator=(std::shared_ptr&& r) std::shared_ptr(std::move(r)).swap(*this)ยังคงมาจากมาตรฐานตัวสร้างการเคลื่อนไหวของstd::shared_ptr(std::shared_ptr&& r)รถrว่างจึงrว่างเปล่า ( r.get() == nullptr) swapก่อนที่จะเรียกร้องให้ ในกรณีของฉันหมายถึงnext->nextสิ่งนี้ว่างเปล่าก่อนวัตถุเก่าที่ถูกชี้nextถูกทำลาย (โดยการswapเรียก)
โฮลต์

1
@ VF1 รหัสของคุณจะไม่เหมือนกัน - เรียกร้องให้fอยู่ในnextที่ไม่ได้next->nextและตั้งแต่next->nextเป็นโมฆะจะหยุดทันที
โฮลต์

1

ความจริงแล้วฉันไม่คุ้นเคยกับอัลกอริธึมการจัดสรรคืนตัวชี้สมาร์ทของคอมไพเลอร์ C ++ ใด ๆ แต่ฉันสามารถจินตนาการอัลกอริธึมที่เรียบง่ายและไม่เรียกซ้ำได้ พิจารณาสิ่งนี้:

  • คุณมีคิวพอยน์เตอร์อัจฉริยะรอการจัดสรรคืน
  • คุณมีฟังก์ชั่นที่ใช้ตัวชี้แรกและยกเลิกการจัดสรรและทำซ้ำสิ่งนี้จนกว่าคิวจะว่างเปล่า
  • หากตัวชี้สมาร์ทต้องการยกเลิกการจัดสรรตำแหน่งจะถูกส่งไปยังคิวและเรียกใช้ฟังก์ชันข้างต้น

ดังนั้นจึงไม่มีโอกาสที่สแต็กจะโอเวอร์โฟลว์และมันง่ายกว่ามากที่จะปรับอัลกอริทึมแบบเรียกซ้ำ

ฉันไม่แน่ใจว่าสิ่งนี้สอดคล้องกับปรัชญา "ตัวชี้สมาร์ทราคาเกือบเป็นศูนย์" หรือไม่

ฉันเดาว่าสิ่งที่คุณอธิบายจะไม่ทำให้เกิดการล้นสแต็ค แต่คุณสามารถลองสร้างการทดสอบที่ชาญฉลาดเพื่อพิสูจน์ว่าฉันผิด

UPDATE

นี่เป็นข้อผิดพลาดที่ฉันเขียนไว้ก่อนหน้านี้:

#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;
}

โปรแกรมนี้สร้างขึ้นชั่วนิรันดร์และแยกแยะสายโซ่ของโหนด มันทำให้เกิดการล้นสแต็ค


1
อาคุณหมายถึงการใช้สไตล์การสานต่อแบบต่อเนื่องหรือไม่? อย่างมีประสิทธิภาพนั่นคือสิ่งที่คุณกำลังอธิบาย อย่างไรก็ตามฉันจะเสียสละพอยน์เตอร์สมาร์ทเร็วกว่าสร้างรายการอื่นบนกองเพื่อยกเลิกการจัดสรรรายการเก่า
VF1

ฉันผิดไป. ฉันเปลี่ยนคำตอบแล้ว
Gábor Angyal
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.