2013-07-22 8 views
24

仕様でテールコールの最適化を行っている真のRAII言語は考えられませんが、実装固有の最適化として多くのC++実装で可能です。テールコール最適化とRAII共存可能?

これが行うこれらの実装のための質問を提起:再帰呼び出しをしなければならないことを、別ガベージコレクションルーチンによってないデストラクタが自動変数のスコープの終わりに呼び出されることを考えると、それはTCOの制約に違反しません関数の最後にある最後の命令ですか?例えば

: -

#include <iostream> 

class test_object { 
public: 
    test_object() { std::cout << "Constructing...\n"; } 
    ~test_object() { std::cout << "Destructing...\n"; } 
}; 

void test_function(int count); 

int main() 
{ 
    test_function(999); 
} 

void test_function(int count) 
{ 
    if (!count) return; 
    test_object obj; 
    test_function(count - 1); 
} 

その後、999回を書かれ、されるだろう "...の構築" "消滅..." 別の999倍。結局のところ、999 test_objectインスタンスはアンワインドする前に自動的に割り当てられます。しかし、実装にTCOがあると仮定すると、1000のスタックフレームが存在するのでしょうか?

再帰呼び出し後のデストラクタは、デファクトTCO実装要件と衝突しますか?

+3

実際に面白いですが。私は「もちろんTCOを防ぐことはできません」と言うつもりだったが、もっと見ると、もっとそうだと思う。 –

+2

ここでテールコールはありません。そのままコンパイルしてください)。理論的には良いコンパイラがパターンに気づき、これを2つのループに変えることができます。 –

+5

短い答え:はい、確定的なデストラクタはTCOを防ぎます。長い答え:実際にはいくつかのコンパイラ(私が信じるLLVMなど)はTCOのより許容された形式を実装し、より多くのケースを許容するかもしれません。 –

答えて

13

TCOに対してはRAIのように見えるでしょう。しかし、コンパイラが "それを取り除く"ことができる方法は数多くあることに留意してください。

デストラクタが簡単で、デフォルトのデストラクタ(コンパイラ生成)であり、すべてのサブオブジェクトが些細なデストラクタでもある場合、デストラクタは事実上存在しません(常に最適化された)。その場合、通常通りにTCOを実行することができます。

次に、デストラクタをインライン化することができます(コードは関数のように呼び出されるのではなく、関数に直接渡されます)。その場合、return文の後に "クリーンアップ"コードがあるだけです。コンパイラは、最終結果が同じであるかどうかを判断できる場合(「if-if」ルール)、再順序付けによってより良いコードが得られる場合(一般的にはそうする)、操作を並べ替えることができます。 TCOは、ほとんどのコンパイラで適用されている考慮事項の1つであると想定します(つまり、コードがTCOに適したものになるように並べ替えることができれば、それが実行されます)。

コンパイラがそれ自身でそれを行うのに十分にスマートにできない場合の残りの場合は、プログラマの責任となります。この自動デストラクタ呼び出しが存在するため、テールコール後にTCOを禁止するクリーンアップコードをプログラマが見るのが少し難しくなりますが、プログラマがテールコールを作成する能力の点で違いはありませんTCOの候補として機能します。たとえば:

void RAII_recursion(int a) { 
    std::vector<int> arr(a); 
    // do some stuff with vector "arr" 
    RAII_recursion(--a); // tail-call 
}; // arr gets destroyed here, not good for TCO. 

しかし、ベクトルのデストラクタがインライン化されない限り、賢いプログラマは、まだ可能性がある、(これは動作しないことがわかります。

void nonRAII_recursion(int a) { 
    int* arr = new int[a]; 
    // do some stuff with array "arr" 
    delete[] arr; 
    nonRAII_recursion(--a); // tail-call 
}; 

さて、ナイーブRAII_recursion実装があるかもしれませんこの場合)、簡単に状況を是正することができます

void RAII_recursion(int a) { 
    { 
    std::vector<int> arr(a); 
    // do some stuff with vector "arr" 
    }; // arr gets destroyed here 
    RAII_recursion(--a); // tail-call 
}; 

をそして私はあなたがこの種ののない場合は、基本的に存在しないことを示している可能性がかなり確信していますトリックを使用してTCOを確実に適用することはできませんでした。したがって、RAIIは、TCOを適用できるかどうかを確認するのが少し難しくなります。しかし、私は、TCO対応再帰呼び出しを設計するのに十分な賢明なプログラマーは、テールコールの前に強制的に実行する必要のある「隠された」デストラクタ呼び出しを見るのに十分な立場にあると考えています。

注:このように見て、デストラクタは自動クリーンアップコードを隠します。クリーンアップコード(つまり、非自明なデストラクタ)が必要な場合は、RAIIを使用するかどうかにかかわらず(たとえばCスタイルの配列など)、必要になります。そして、TCOを可能にしたい場合は、テールコール(RAIIの有無にかかわらず)を行う前にクリーンアップを行うことが可能でなければならず、RAIIオブジェクトを強制的に破棄することは可能ですテールコールの前に(例えば、それらを余分なスコープの中に入れて)。

+1

もしベクトルのデストラクタが外部から見える副作用を持っているとすれば、C++標準ではすべてが構築されていなければならないと思いますが、すべて逆の順序で破壊されますか? 'arr'のデストラクタが副作用を起こすかどうかを知るには、' arr'へのポインタや参照を受け取るコードをすべて解析する必要があります。場合によっては可能かもしれませんが、すべてではありません。 – supercat

+2

@supercat:標準では、観察可能な振る舞いを指定しています(つまり、逆順で破壊されたように振る舞います)。シュレディンガーの猫の問題です。実行の順序を観察可能にするとすぐに標準でそのコードを削除すると、あなたが観察できないものの順序を確かめることはできません。 –

+1

@supercat:「外部から見える副作用」を持つベクターを破壊することはもちろん、コンパイラと状況に依存します。確かにいつも決めることはできません。また、どのようにスマートなコンパイラがw.r.t. mem-allocationも重要です(例えば、可能な副作用を伴う単なる関数呼び出しと見えますか、目に見える副作用としてのヒープへの変更を考慮しますか?)、そしてコンパイラはいくつかの自由を持っていますそれ。私は、クリーンアップコードが安全に再注文することは難しいことに同意します。 –

4

コンパイラがTCOを実行すると、デストラクタが呼び出される順序は、TCOを実行しないときに変更されます。

この並べ替えが問題ではないことがコンパイラによって証明された場合(デストラクタが自明でない場合など)、と同様にルールに従って、TCOを実行できます。しかし、あなたの例では、コンパイラはそれを証明することはできませんし、TCOをしません。

+1

RAIIの場合、ほとんどの場合、それは重要であるため、並べ替えが問題ではないことを証明することはできません。 –

+1

@James Kanze。確かに。デストラクタ*がリソースを解放するという意味でRAIIを参照すると(これは最も受け入れられる意味です)、TCOを実行することは不可能です。より広い意味でのRAIIを見れば、現在のスコープを離れるときにデストラクタが呼び出され、場合によってはTCOを実行することも可能です。 –

+1

何もしないデストラクタはRAIIではありません。一般的に、トリビアルでないコンストラクタやデストラクタが存在すると、TCOが抑制されます。 –

関連する問題