🆓

いろいろな言語に用意されたリソース解放の仕組み

2024/11/05に公開

C++

C++の前身であるC言語の時代から、多くのプログラマがポインタのメモリ解放漏れによるメモリリークに悩まされてきました。しかし、C++でクラスのコンストラクタ/デストラクタが使えるようになり RAII (Resource Acquisition Is Initialization) の考え方が導入されたことで、ようやくプログラマはリソース解放の責任から逃れることができるようになりました。
もちろん、お作法を守らなければすぐリークしますので要注意なことに変わりはありませんが。

RAII が取り入れられた代表的なクラスとして、std::unique_ptr std::shared_ptr std::ifstream などがあります。これらのクラスは、デストラクタで自身が所有するメモリやリソースを解放してくれるように設計されています。デストラクタで解放されるということは、そのオブジェクトが破棄されるタイミング、すなわちスコープを抜けたタイミングで解放されるということです。

int main() {
    ...

    {
        // ファイル foo.txt をオープン
        std::ifstream file("foo.txt");  // 🚩RAIIオブジェクトを用いてファイルオープン
        std::string line;

        // ファイルを読み込む
        while (std::getline(file, line)) {
            std::cout << line << std::endl;
        }
    }  // ⭕このブロックの終了時に file がクローズされる(デストラクタが呼び出される)

    ...
}

スコープは関数のブロックであることが多いですが、上の例のように任意の位置にブロックを設けることでリソース解放のタイミングを制御することも可能です。

Go

GoにはGC (Garbage Collection) があるため、変数のメモリ解放を意識する必要はありませんが、ファイルなどのリソースの解放(後処理)を自動的に行う仕組みはなく、明示的にコードを書く必要があります。また、クラスやデストラクタが存在しないのでC++のようなRAIIの考えを取り入れることもできません。しかし、後処理のための便利な仕組みとしてdeferステートメントが用意されています。これは他の言語にはない独特のステートメントです。

deferに登録された関数は、deferが書かれた関数の終了時に呼び出されます。
注意すべきなのは、deferが書かれたブロックの終端で呼び出される訳ではないという点で、他の言語から入った人は少し誤解しやすいところかと思います。このお陰で、ifやforなどのブロック内でdeferを書いても、きちんと関数の終了時に動作してくれます。

func main() {
    ...

    {
        // ファイル foo.txt をオープン
        file, err := os.Open("foo.txt")
        if err == nil {
            paic(err.Error())
        }
        defer file.Close()  // 🚩defer でファイルクローズを予約

        // ファイルを読み込む
        scanner := bufio.NewScanner(file)
        for scanner.Scan() {
            fmt.Println(scanner.Text())
        }
    }  // ❌このブロック終了時には file.Close() は呼び出されない

    ...
}  // ⭕関数 main の終了時に file.Close() が呼び出される

他の言語と同じように、ブロック(スコープ)終了時に関数を呼び出させたい場合は、次のように無名関数+即時呼び出しにすることで実現ができます。

func main() {
    ...

    func() {
        // ファイル foo.txt をオープン
        file, err := os.Open("foo.txt")
        if err == nil {
            paic(err.Error())
        }
        defer file.Close()  // 🚩defer で file のクローズを予約

        // ファイルを読み込む
        scanner := bufio.NewScanner(file)
        for scanner.Scan() {
            fmt.Println(scanner.Text())
        }
    }()  // ⭕無名関数の終了時に file.Close() が呼び出される

    ...
}

Python

PythonにもGCがあるため、変数のメモリ解放を意識する必要はありません。リソースの解放については、クラスに特殊メソッド __del__ が用意されていますが、これはデストラクタではなくファイナライザであり、GCが動作するタイミングで呼び出されるものです。プログラマが呼び出されるタイミングを制御することができないのでRAIIには利用できません。しかし、Pythonにはファイルなどのリソースの解放(後処理)を自動的に行う専用の仕組みが用意されています。それは、コンテキストマネージャという仕組みで、with ステートメントを用いてコードを書きます。

def main():
    ...

    # 🚩withステートメントを使用して foo.txt をオープン
    with open("foo.txt", "r") as file:
        # ファイルを読み込む
        for line in file.readlines():
            print(line)

    # ⭕with ブロックを抜けると file がクローズされる
    ...

withステートメントで利用されるオブジェクト(上の例では file)は、__enter____exit__ という特殊メソッドを実装している必要があります。

withステートメントを用いると、オブジェクト生成のためにまず __enter__ が呼び出され、最後にブロックを抜けるときに __exit__ が呼び出されます。そのため、オブジェクトのクラスの実装で、__enter__ が呼び出されたらオブジェクトを生成して返し、__exit__ が呼び出されたらリソースを解放するようにしておくことで、コンテキストマネージャによるメモリ管理の仕組みを利用することができます。

VBA

おまけ。

VBAのクラスにもデストラクタがあるので、自作クラスにRAIIの考えを取り入れることができます。関数のスコープか、またはWithステートメントを用いて任意のブロックでリソース解放を行うことができます。

前述のサンプルコードとは異なりファイルオープンの例ではないですが、次のようなイメージです。

Function Main()
    ' 🚩AObjectを生成
    Dim a As New AObject
    ...

    ' 🚩BObjectを生成
    With New BObject
        ...
    End With  ' ⭕BObjectが解放される(デストラクタが呼び出される)

    ...
End Function  ' ⭕a (AObject) が解放される(デストラクタが呼び出される)

Discussion