大きなリストを破棄すると、スタックがオーバーフローしますか?


11

次の単一リンクリストの実装を検討してください。

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

次に、std::unique_ptr<node> headスコープ外になり、デストラクタが呼び出されるインスタンスの使用を停止するとします。

これは十分に大きなリストのスタックを爆破しますか?それは公正なコンパイラが(インラインかなり複雑な最適化を行いますと仮定することであるunique_ptr「内のデストラクタをnode私がしなければ以来、以下の(はるかに困難になっている、そして、s」は末尾再帰を使用)dataデストラクタがわかりにくくなりnext、のハードそれを作りますコンパイラーが再配列の可能性と末尾呼び出しの機会に気付くように)

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

dataどういうわけかnodeそれへのポインタがある場合、それはテール再帰が不可能であることさえあるかもしれません(もちろん、私たちはそのようなカプセル化の違反を回避するよう努めるべきです)。

一般的に、そうでない場合、このリストを破棄するにはどうすればよいですか?共有ポインタにはrelease!がないため、リストを走査して「現在の」ノードを削除することはできません。唯一の方法は、カスタムの削除ツールを使用することです。


1
2番目のケースで述べたカプセル化の違反がなくても、それが価値があることについては、gcc -O3(再帰的な複雑な例で)末尾再帰を最適化できませんでした。
VF1、2015年

1
そこにあなたの答えがあります:コンパイラが再帰を最適化できなければ、スタックを破壊するかもしれません。
Bart van Ingen Schenau、2015年

@BartvanIngenSchenau これはこの問題の別の例だと思います。スマートポインターの清潔さが好きなので、これも本当の恥です。
VF1 2015年

回答:


6

はい、コンパイラがたまたまnodeデストラクタ デストラクタに末尾呼び出し最適化を適用しない限り、これは最終的にスタックをshared_ptr破壊します。後者は、標準ライブラリの実装に大きく依存しています。たとえば、MicrosoftのSTLはこれを実行しません。これは、shared_ptr最初にその参照先の参照カウントを減らし(おそらくオブジェクトを破壊して)、次にその制御ブロックの参照カウント(弱い参照カウント)を減らします。したがって、内部デストラクタは末尾呼び出しではありません。これは仮想呼び出しでもあり、最適化される可能性がさらに低くなります。

典型的なリストでは、1つのノードが次のノードを所有するのではなく、すべてのノードを所有する1つのコンテナを使用することでこの問題を回避し、ループを使用してデストラクタ内のすべてを削除します。


ええ、私は最終的にそれらshared_ptrのsのカスタム削除機能を使用して「典型的な」リスト削除アルゴリズムを実装しました。スレッドセーフが必要なため、ポインタを完全に取り除くことはできません。
VF1 2015年

私は共有ポインターの「カウンター」オブジェクトが仮想デストラクタを持っていることも知りませんでした、私はいつもそれが強い参照+弱い参照+削除を保持する単なるPODであると仮定しました...
VF1

@ VF1ポインタが必要なスレッドセーフティを確実に提供していますか?
Sebastian Redl、2015年

はい-それstd::atomic_*は彼らのためのオーバーロードの要点です、いいえ?
VF1 2015年

はい、しかしそれはあなたが達成することもできないものstd::atomic<node*>であり、より安いです。
Sebastian Redl、2015年

5

遅い答えですが、誰もそれを提供しなかったので...同じ問題に遭遇し、カスタムデストラクタを使用して解決しました:

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

実際にリストがある場合、つまりすべてのノードの前に1つのノードがあり、多くても1人のフォロワーがいて、あなた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の要件を満たしているかどうかはわかりませんが、他の点で問題に取り組むための確かな方法です。
ジュール

move演算子をオーバーロードしない限り、このアプローチが実際に何を保存するかはわかりません。実際のリストでは、各while条件が最大で1回評価され、再帰的にnext = std::move(next->next)呼び出されnext->~node()ます。
VF1 2016年

1
@ VF1これは、next->nextが指す値nextが破棄される前に(移動割り当て演算子によって)が無効になり、再帰が「停止」するため機能します。私は実際にこのコードと、この作品(でテスト使用しg++clangかつmsvc移動ポインタが尖った古いオブジェクトを破壊する前に無効化されているという事実を)を、今あなたがそれを言うことを、私は(これは、標準で定義されていることを確認していませんターゲットポインタによる)。
Holt

@ VF1更新:標準によると、operator=(std::shared_ptr&& r)と同等std::shared_ptr(std::move(r)).swap(*this)です。まだ標準から、のmoveコンストラクターstd::shared_ptr(std::shared_ptr&& r)r空になるため、の呼び出し前rはempty(r.get() == nullptrswapです。私の場合、これは、next->nextが指す古いオブジェクトnextが(swap呼び出しによって)破棄される前は空であることを意味します。
Holt

1
@ VF1コードは同じではありません-への呼び出しfはon nextではなくon でありnext->nextnext->nextnullなので、すぐに停止します。
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 2015年

私は間違っていた。私はそれに応じて私の答えを変更しました。
ガーボルアンギャル
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.