큰 목록을 파괴하면 스택이 오버플로됩니까?


12

다음과 같은 단일 연결 목록 구현을 고려하십시오.

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

이제 std::unique_ptr<node> head인스턴스를 사용하지 않고 범위를 벗어난 소멸자를 호출 한다고 가정 해보십시오 .

이것이 충분히 큰 목록에 대해 내 스택을 날려 버립니까? 이 공정은 컴파일러 (인라인 꽤 복잡 최적화를 할 것이라고 가정하는 것입니다 unique_ptr'로 소멸자를 node내가 할 경우 이후 다음 (더 힘들어 얻을 수있는, 다음의'꼬리 재귀를 사용) data소멸자 당황 것 next'의 하드를 만들기 컴파일러가 잠재적 재정렬 및 ​​테일 콜 기회를 알 수 있도록) :

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

data어떻게 든 그것의 포인터가 있다면 node꼬리 재귀가 불가능할 수도 있습니다 (물론 캡슐화의 위반을 피하기 위해 노력해야 함).

일반적으로 어떻게하면이 목록을 어떻게 파괴해야합니까? 공유 포인터에는 release! 가 없으므로 목록을 탐색하고 "현재"노드를 삭제할 수 없습니다 . 유일한 방법은 사용자 정의 삭제 도구를 사용하는 것입니다.


1
두 번째 경우에서 언급 한 캡슐화가 위반되지 않아도 가치가 gcc -O3있는 것은 꼬리 재귀를 최적화 할 수 없었습니다 (복잡한 예에서).
VF1

1
거기에 답이 있습니다. 컴파일러가 재귀를 최적화하지 못하면 스택이 날려 버릴 수 있습니다.
Bart van Ingen Schenau

@BartvanIngenSchenau 이 문제 의 또 다른 사례 인 것 같습니다 . 스마트 포인터 청결도를 좋아하기 때문에 정말 부끄러운 일입니다.
VF1

답변:


7

예, 컴파일러가 node의 소멸자 소멸자에 꼬리 호출 최적화를 적용하지 않는 한 결국 스택을 날려 버릴 것 shared_ptr입니다. 후자는 표준 라이브러리 구현에 크게 의존합니다. 예를 들어, Microsoft의 STL은 shared_ptr해당 대상의 참조 카운트를 먼저 줄인 다음 (객체를 파괴 할 수 있음) 제어 블록의 참조 카운트 (약한 참조 카운트)를 줄 이므로 절대로 그렇게하지 않습니다 . 따라서 내부 소멸자는 꼬리 호출이 아닙니다. 또한 가상 호출 이므로 최적화 될 가능성이 훨씬 적습니다.

일반적인 목록은 하나의 노드가 다음 노드를 소유하지 않고 모든 노드를 소유하는 하나의 컨테이너를 가지며 루프를 사용하여 소멸자의 모든 것을 삭제하여 문제를 해결합니다.


네, 결국에는 일반 사용자 지정 삭제 도구를 사용하여 "일반"목록 삭제 알고리즘을 구현했습니다 shared_ptr. 스레드 안전이 필요했기 때문에 포인터를 완전히 제거 할 수 없습니다.
VF1

나는 공유 포인터 "카운터"객체가 가상 소멸자를 가질 것이라는 것을 몰랐다. 나는 항상 그것이 강한 심판 + 약한 심판 + 삭제기를 들고있는 POD라고 생각했다.
VF1

@ VF1 포인터가 원하는 스레드 안전성을 제공한다고 확신하십니까?
Sebastian Redl

그렇습니다- std::atomic_*그것들 의 과부하의 요점입니다 .
VF1

예, 그러나 그것은 당신이 달성 할 수없는 것이 아니며 std::atomic<node*>저렴합니다.
Sebastian Redl

5

늦은 대답이지만 아무도 그것을 제공하지 않았기 때문에 ... 같은 문제가 발생하여 사용자 정의 소멸자를 사용하여 해결했습니다.

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의 요구 사항을 충족시키는지는 확실하지 않지만 다른 측면에서 문제에 접근하는 좋은 방법입니다.
Jules

이동 연산자에 과부하가 걸리지 않는 한이 방법이 실제로 실제로 목록을 저장하는 방법을 잘 모르겠습니다. 각 목록은 조건을 최대 한 번 평가하고 재귀 적 으로 next = std::move(next->next)호출 next->~node()합니다.
VF1

1
@ VF1 이것은 next->next지정된 값 next이 파괴 되기 전에 (이동 할당 연산자에 의해) 무효화 되어 재귀를 "중지"하기 때문에 작동합니다. 실제로이 코드 와이 작업 ( g++, clang및으로 테스트 됨 msvc)을 사용하지만 이제는 표준에 의해 정의되었는지 확실하지 않습니다 (이전 객체가 파괴되기 전에 이동 된 포인터가 무효화된다는 사실) 대상 포인터로).
Holt

@ 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합니다.
Holt

1
@ VF1 코드는 동일하지 않습니다 - 호출은 할 fnext,하지 next->next이후, 그리고 next->next널 (null)이 즉시 중지합니다.
Holt

1

솔직히 말하면 나는 C ++ 컴파일러의 스마트 포인터 할당 해제 알고리즘에 익숙하지 않지만 이것을 수행하는 간단한 비 재귀 알고리즘을 상상할 수 있습니다. 이걸 고려하세요:

  • 할당 해제를 기다리는 스마트 포인터 대기열이 있습니다.
  • 첫 번째 포인터를 가져 와서 할당을 해제하고 큐가 비워 질 때까지 이것을 반복하는 함수가 있습니다.
  • 스마트 포인터가 할당 해제가 필요한 경우 대기열로 푸시되고 위 함수가 호출됩니다.

따라서 스택이 오버플로 될 가능성이 없으며 재귀 알고리즘을 최적화하는 것이 훨씬 간단합니다.

이것이 "거의 제로 비용 스마트 포인터"철학에 맞는지 확실하지 않습니다.

나는 당신이 묘사 한 것이 스택 오버플로를 유발하지는 않을 것이라고 생각하지만, 당신은 나를 잘못 증명하는 영리한 실험을 시도 할 수 있습니다.

최신 정보

글쎄, 이것은 내가 이전에 쓴 내용이 잘못되었음을 증명합니다.

#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.