C++
 Computer >> コンピューター >  >> プログラミング >> C++

C++のshared_ptrで仮想デストラクタなしでも正しく破棄できる理由を解説

この記事では、C++における std::shared_ptr を使ったオブジェクトの破棄(仮想破壊)の仕組みについて、サンプルコードを交えながらわかりやすく解説します。

通常、基底クラスのポインタ経由で派生クラスのインスタンスを削除する場合、基底クラスのデストラクタを virtual に定義する必要があります。これにより、オブジェクトは生成された順序とは逆に、派生クラスから基底クラスへと正しく破棄されます。しかし、shared_ptr を使うと、仮想デストラクタがなくても適切なデストラクタが呼び出されるという特徴があります。

サンプルコード

#include <iostream>
#include <memory>
using namespace std;

class Base {
public:
    Base() {
        cout << "Constructing Base" << endl;
    }
    ~Base() {
        cout << "Destructing Base" << endl;
    }
};

class Derived : public Base {
public:
    Derived() {
        cout << "Constructing Derived" << endl;
    }
    ~Derived() {
        cout << "Destructing Derived" << endl;
    }
};

int main() {
    std::shared_ptr<Base> sp{ new Derived };
    return 0;
}

実行結果

Constructing Base
Constructing Derived
Destructing Derived
Destructing Base

なぜ正しく破棄されるのか?

上記のコードでは、Base クラスのデストラクタに virtual を付けていないにもかかわらず、実行結果では Derived のデストラクタが先に呼び出され、その後に Base のデストラクタが呼び出されています。

これは、shared_ptr生成時に渡された実際の型情報を保持する「型消去(type erasure)」されたデリータを持っているためです。std::shared_ptr<Base> sp{ new Derived }; のように構築した時点で、Derived 型を正しく解放するための削除処理が内部に記録されます。そのため、スコープを抜けて sp が破棄される際には、ポインタの静的な型(Base*)に関係なく、Derived のデストラクタが適切に呼び出されます。

生ポインタや unique_ptr との違い

一方、生ポインタを使って delete basePtr; のように削除した場合や、unique_ptr<Base> を同じ方法で構成した場合は、基底クラスのデストラクタが仮想でないと未定義動作(undefined behavior)となり、派生クラスのデストラクタが呼ばれない可能性があります。

つまり、shared_ptr はこの点において安全な設計になっていますが、可読性や保守性の観点からは、多態的なクラス階層を持つ基底クラスには引き続き virtual デストラクタを定義しておくことがベストプラクティスとされています。

まとめ

  • shared_ptr は生成時の実際の型を記憶するため、仮想デストラクタがなくても派生クラスを正しく破棄できる。
  • 生ポインタや unique_ptr では、仮想デストラクタがないと未定義動作になる恐れがある。
  • 安全性とコードの一貫性のため、基底クラスには virtual デストラクタを付ける習慣を持とう。
  1. C++で解く!Nの階乗のB進表現における末尾ゼロの個数の求め方

    はじめにこの記事では、与えられた数Nの階乗(N!)を基数Bで表したとき、末尾にいくつのゼロが連続するかを求める問題について詳しく解説します。問題の例入力 : N = 7、基数 = 2 出力 : 4 説明 : fact(7) = 5040(10進数)であり、2進数では「1001110110000」となるため、末尾にゼロが4個並びます。 入力 : N = 11、基数 = 5 出力 : 2 説明 : fact(11) = 39916800(10進数)であり、5進数では「40204314200」となるため、末尾にゼロが2個並びます。基数変換のおさらいまず、10進数から他の基数へ数値を変換する手順を確

  2. C++で解く!Nの階乗の16進数表現における末尾のゼロの個数の求め方

    この記事では、与えられた整数Nの階乗(N!)を16進数で表したとき、末尾に何個のゼロが連続するかを求める問題について詳しく解説します。 入力 : N = 7 出力 : 1 説明 : fact(7) = 5040(10進数)で、16進数では13B0となり、末尾のゼロは1個です。 入力 : N = 11 出力 : 2 説明 : fact(11) = 39916800(10進数)で、16進数では2611500となり、末尾のゼロは2個です。 10進数から16進数への変換のおさらい まず、任意の10進数を別の基数へ変換する手順をおさらいしましょう。ここでは、(5040)10 を16進数に変換する例を