Sẽ phá hủy một danh sách lớn tràn qua ngăn xếp của tôi?


11

Hãy xem xét việc thực hiện danh sách liên kết đơn lẻ sau đây:

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

Bây giờ, giả sử tôi ngừng sử dụng một số std::unique_ptr<node> headtrường hợp sau đó đi ra khỏi phạm vi, khiến cho hàm hủy của nó được gọi.

Điều này sẽ thổi chồng của tôi cho danh sách đủ lớn? Có công bằng không khi cho rằng trình biên dịch sẽ thực hiện tối ưu hóa khá phức tạp (hàm unique_ptrhủy của nội tuyến thành node, sau đó sử dụng đệ quy đuôi), sẽ khó hơn nhiều nếu tôi làm như sau (vì hàm datahủy sẽ làm nextkhó, làm cho nó khó để trình biên dịch nhận thấy cơ hội sắp xếp lại và gọi đuôi tiềm năng):

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

Nếu databằng cách nào đó có một con trỏ tới nó nodethì thậm chí có thể không thể đệ quy đuôi (mặc dù tất nhiên chúng ta nên cố gắng tránh các vi phạm đóng gói như vậy).

Nói chung, làm thế nào để có thể phá hủy danh sách này nếu không? Chúng tôi không thể duyệt qua danh sách và xóa nút "hiện tại" vì con trỏ dùng chung không có a release! Cách duy nhất là với một deleter tùy chỉnh, nó thực sự có mùi với tôi.


1
Đối với những gì nó có giá trị, ngay cả khi không vi phạm đóng gói được đề cập trong trường hợp thứ hai, gcc -O3không thể tối ưu hóa đệ quy đuôi (trong một ví dụ phức tạp).
VF1

1
Ở đó bạn có câu trả lời của mình: Nó có thể thổi bay ngăn xếp của bạn, nếu trình biên dịch không thể tối ưu hóa đệ quy đi.
Bart van Ingen Schenau 27/1/2015

@BartvanIngenSchenau Tôi đoán đó là một ví dụ khác của vấn đề này . Đó cũng là một sự xấu hổ thực sự, vì tôi thích sự sạch sẽ của con trỏ thông minh.
VF1

Câu trả lời:


6

Vâng, điều này cuối cùng sẽ thổi bay ngăn xếp của bạn, trừ khi trình biên dịch chỉ tình cờ áp dụng tối ưu hóa cuộc gọi đuôi cho hàm nodehủy và hàm shared_ptr hủy của. Thứ hai là rất phụ thuộc vào việc thực hiện thư viện tiêu chuẩn. Chẳng hạn, STL của Microsoft sẽ không bao giờ làm điều đó, vì shared_ptrtrước tiên sẽ giảm số tham chiếu của người được chỉ định (có thể phá hủy đối tượng) và sau đó giảm số tham chiếu của khối điều khiển (số tham chiếu yếu). Vì vậy, hàm hủy bên trong không phải là một cuộc gọi đuôi. Đây cũng là một cuộc gọi ảo , khiến cho nó thậm chí ít có khả năng sẽ được tối ưu hóa.

Các danh sách điển hình khắc phục vấn đề đó bằng cách không có một nút nào sở hữu nút kế tiếp, nhưng bằng cách có một thùng chứa tất cả các nút và sử dụng một vòng lặp để xóa mọi thứ trong hàm hủy.


Vâng, tôi đã triển khai thuật toán xóa danh sách "điển hình" với một deleter tùy chỉnh cho những shared_ptrcuối cùng. Tôi hoàn toàn không thể thoát khỏi các con trỏ vì tôi cần sự an toàn của luồng.
VF1

Tôi cũng không biết đối tượng "bộ đếm" con trỏ được chia sẻ cũng sẽ có một hàm hủy ảo, tôi luôn cho rằng nó chỉ là một POD giữ các ref mạnh + refs yếu + deleter ...
VF1

@ VF1 Bạn có chắc chắn các con trỏ đang cung cấp cho bạn sự an toàn của luồng mà bạn muốn không?
Sebastian Redl

Có - đó là toàn bộ vấn đề std::atomic_*quá tải cho họ, phải không?
VF1

Có, nhưng đó không phải là thứ bạn không thể đạt được std::atomic<node*>, và rẻ hơn.
Sebastian Redl

5

Câu trả lời muộn nhưng vì không ai cung cấp nó ... Tôi đã gặp vấn đề tương tự và giải quyết nó bằng cách sử dụng một hàm hủy tùy chỉnh:

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

Nếu bạn thực sự có một danh sách , tức là mọi nút được đi trước bởi một nút và có nhiều nhất một người theo dõi, và bạn listlà một con trỏ đến đầu tiên node, ở trên sẽ hoạt động.

Nếu bạn có một số cấu trúc mờ (ví dụ: biểu đồ chu kỳ), bạn có thể sử dụng như sau:

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

Ý tưởng là khi bạn làm:

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

Con trỏ chia sẻ cũ nextbị hủy (vì use_countbây giờ là con trỏ 0) và bạn chỉ đến phần sau. Điều này thực hiện chính xác giống như hàm hủy mặc định, ngoại trừ nó lặp đi lặp lại thay vì đệ quy và do đó tránh tràn ngăn xếp.


Ý tưởng thú vị. Không chắc chắn rằng nó đáp ứng yêu cầu của OP về an toàn luồng, nhưng chắc chắn là một cách tốt để tiếp cận vấn đề ở các khía cạnh khác.
Jules

Trừ khi bạn làm quá tải toán tử di chuyển, tôi không chắc cách tiếp cận này thực sự tiết kiệm bất cứ thứ gì - trong một danh sách thực, mỗi điều kiện sẽ được đánh giá nhiều nhất một lần, bằng cách next = std::move(next->next)gọi next->~node()đệ quy.
VF1

1
@ VF1 Điều này hoạt động vì next->nextbị vô hiệu hóa (bởi toán tử gán di chuyển) trước khi giá trị được trỏ bởi nextbị hủy, do đó "dừng" đệ quy. Tôi thực sự sử dụng mã này và công việc này (đã được thử nghiệm với g++, clangmsvc), nhưng bây giờ bạn nói nó, tôi không chắc chắn rằng điều này được xác định bởi tiêu chuẩn (thực tế là con trỏ di chuyển bị vô hiệu trước khi phá hủy đối tượng cũ đã chỉ bởi con trỏ đích).
Holt

@ Cập nhật VF1: Theo tiêu chuẩn, operator=(std::shared_ptr&& r)tương đương với std::shared_ptr(std::move(r)).swap(*this). Vẫn từ tiêu chuẩn, hàm tạo di chuyển std::shared_ptr(std::shared_ptr&& r)làm cho rtrống, do đó rlà trống ( r.get() == nullptr) trước khi gọi đến swap. Trong trường hợp của tôi, điều này có nghĩa next->nextlà trống rỗng trước khi đối tượng cũ được chỉ bởi nextbị phá hủy (bởi swapcuộc gọi).
Holt

1
@ VF1 Mã của bạn không giống nhau - Cuộc gọi đến fđược bật next, không phải next->nextvà vì next->nextlà null nên nó dừng ngay lập tức.
Holt

1

Thành thật mà nói, tôi không quen thuộc với thuật toán phân bổ con trỏ thông minh của bất kỳ trình biên dịch C ++ nào, nhưng tôi có thể tưởng tượng một thuật toán đơn giản, không đệ quy thực hiện điều này. Xem xét điều này:

  • Bạn có một hàng các con trỏ thông minh đang chờ giải quyết.
  • Bạn có một hàm lấy con trỏ đầu tiên và giải phóng nó, và lặp lại điều này cho đến khi hàng đợi trống.
  • Nếu một con trỏ thông minh cần giao dịch, nó sẽ được đẩy vào hàng đợi và hàm trên được gọi.

Do đó, sẽ không có cơ hội cho ngăn xếp tràn ra, và việc tối ưu hóa thuật toán đệ quy sẽ đơn giản hơn nhiều.

Tôi không chắc chắn nếu điều này phù hợp với triết lý "con trỏ thông minh chi phí gần như bằng không".

Tôi đoán rằng những gì bạn mô tả sẽ không gây ra lỗi tràn ngăn xếp, nhưng bạn có thể cố gắng xây dựng một thử nghiệm thông minh để chứng minh tôi sai.

CẬP NHẬT

Vâng, điều này chứng minh sai những gì tôi đã viết trước đây:

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

Chương trình này xây dựng vĩnh viễn và giải mã một chuỗi các nút. Nó gây ra tràn ngăn xếp.


1
Ah, bạn có nghĩa là sử dụng phong cách tiếp tục đi qua? Thực tế, đó là những gì bạn đang mô tả. Tuy nhiên, tôi sẽ sớm hy sinh con trỏ thông minh hơn là xây dựng một danh sách khác trên đống chỉ để giải quyết một danh sách cũ.
VF1

Tôi đã sai. Tôi đã thay đổi câu trả lời của tôi cho phù hợp.
Gábor Angyal
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.