C++ でのリソース管理と RAII

最近、職場でレガシー コード ベースに遭遇しました。このように書かれた素晴らしいクラスがいくつかあります:

class ExtnlOutData
{
public:
    int phase;
    int* phaseIdx;
    char** phaseNames;
    ...

    ExtnlDLLData() : phaseIdx(NULL), phaseNames(NULL) {}

    ~ExtnlDLLData()
    {
        if (phaseIdx) {
            delete[] phaseIdx;
            phaseIdx = NULL;
        }

        if (phaseNames) {
            for (int i = 0; i != phase; ++i) {
                if (phaseNames[i]) delete[] phaseNames[i];
            delete[] phaseNames;
            phaseNames = NULL;
        }
    }
}

実際のクラスはもっと大きいです。このスニペットはさまざまな方法で批判できます。たとえば、コピー操作は浅いコピーを行いますが、これは直観に反します。また、動的ライブラリのプログラマは、単調でエラーが発生しやすいメモリを自分で割り当てる必要があるこのインターフェイスのニーズに準拠しています。 phaseIdx により多くのスペースを割り当てると sizeof(int) * phaseより 、メモリリークが発生します。より少ないスペースを割り当てると、プログラムは不可解にクラッシュします。

今日は「DLL」の部分に焦点を当てたいと思います。このクラスは、当社のソフトウェアのカスタマイズされたプラグインのインターフェイスとして設計されています。次のように使用します:

void calculate ()
{
    ExtnlOutData data;
    extnlDllCalculate(&data);

    // Do something about data
}

問題は、ソフトウェアのすべての部分を、従来の共有ライブラリを構築する同じコンパイラでコンパイルする必要があることです (私たちの場合、それはかなり古い Visual Studio 2008 です)。その理由は、動的ライブラリ内のメモリを割り当てながら、dll の外側のメモリを破棄するためです。異なるコンパイラは異なるメモリ管理関数を呼び出す可能性があるため、プログラムは data のデストラクタでクラッシュします。 .この状況は malloc() を組み合わせるとどうなるかのようなものです と delete 、しかしそれはもっと陰険です。

Qt ライブラリ:例

よく設計されたコード ベースが同様の問題を抱えていることに驚いています。たとえば、Qt ライブラリの親子関係は、同様のリソース管理戦略です。 QT を使用したことがある場合は、次のようなコードを記述しているはずです:

// Not real Qt code
void foo(QString name, QFont font)
{
    QTabWidget parent;
    auto child = new QWidget;
    parent.addTab(child);
    child.setName(name);
    child.setFont(font);
} // The distructor of parent will destory child

結果として、Qt はほとんどのライブラリとは異なり、それ自体がコンパイルしたものとは異なるコンパイラによってリンクすることはできません。たとえば、Windows 64 バイナリ用の QT 5.7.0 には、さまざまなコンパイラ ユーザーを満足させるために 3 つのバージョン (VS 2015、VS 2013、MinGW) があります。 Qt アプリケーションを開発するには、対応するコンパイラを使用する必要があります。

例外の安全性の問題

プログラマーが POSIX プラットフォーム専用のソフトウェアを開発している場合、それは自分の仕事ではないと考えるかもしれません。しかし、私はあなたにも関連する別のポイントがあります。ポイントは、その場しのぎです。 リソース管理戦略は本質的に例外的に安全ではありません。 setName の場合に何が起こるかを考えてみましょう または setFont 例外をスローできます。クライアントによる無害な注文変更により、リークが発生します:

child.setName(name);
child.setFont(font);
// if the above lines throw, the child will never be freed
parent.addTab(child);

QT のような古いスタイルのライブラリが「歴史的な理由」で例外を禁止しているのも不思議ではありません。しかし、ライブラリの作成者は、クライアントが次のようなことを行うことを禁止することはできません:

child.setName(name);
child.setFont(font);
if (!child.valid()) throw Exception{"Invalid tab"}; // May cause leak
parent.addTab(child);

救助するRAII

タイトルでは、独自のリソース管理ルーチンを発明することを思いとどまらせます。その理由は、c++ には既に標準のリソース管理イディオム RAII があるためです。上記のリークや比類のないシステム機能に関する問題を簡単に根絶できます。以下のように最初の例を再設計できます:

struct PhaseData
{
    int ID;
    std::string name;
}

class ExternalOutData
{
public:
    ...

private:
    std::vector<PhaseData> data;
    ...
}

GUI の例については、新しい GUI ライブラリを作成することにした場合、次のようにインターフェイスを設計できます。

void foo(MyString name, MyFont font)
{
    MyTabWidget parent;
    auto child = std::make_unique(MyWidget);
    child.setName(name);
    child.setFont(font);
    parent.addTab(std::move(child));
} // The distructor of parent will destory child

このバージョンはもう少し冗長ですが、Qt と同様の使用法で、Qt の問題はありません。