C++ コア ガイドライン:リソース管理に関するルール

今回と次回の投稿は、おそらくプログラミングにおける最も重要な関心事であるリソース管理に関するものになるでしょう。 C++ コア ガイドラインには、一般的なリソース管理の規則だけでなく、特に割り当てと解放、およびスマート ポインターの規則もあります。今日は、リソース管理の一般的な規則から始めます。

初めに。リソースとはリソースは、管理する必要があるものです。つまり、リソースが限られているため、またはリソースを保護する必要があるため、取得して解放する必要があります。限られた量のメモリ、ソケット、プロセス、またはスレッドしか持つことができません。ある時点で、1 つのプロセスだけが共有ファイルを書き込めるか、1 つのスレッドが共有変数を書き込めます。プロトコルに従わない場合、多くの問題が発生する可能性があります。

あなたのシステムは

  • メモリ リークによりメモリ不足になる。
  • 共有変数を使用する前にロックを取得するのを忘れたため、データ競合が発生します。
  • 異なる順序でいくつかの共有変数を取得および解放しているため、デッドロックが発生しています。

データ競合とデータ ロックの問題は、共有変数に固有のものではありません。たとえば、ファイルでも同じ問題が発生する可能性があります。

リソース管理について考える場合、すべては 1 つの重要なポイント、つまり所有権に帰着します。ですから、ルールについて書く前に、まず全体像を示しましょう。

私が最近の C++ で特に気に入っているのは、所有権に関する意図をコードで直接表現できることです。

  • ローカル オブジェクト .所有者としての C++ ランタイムは、これらのリソースの有効期間を自動的に管理します。同じことが、グローバル オブジェクトまたはクラスのメンバーにも当てはまります。ガイドラインでは、スコープ オブジェクトと呼んでいます。
  • 参考文献 :私は所有者ではありません。空にできないリソースだけを借りました。
  • 生のポインタ :私は所有者ではありません。空にできるリソースだけを借りました。リソースを削除してはなりません。
  • std::unique_ptr :私はリソースの独占所有者です。リソースを明示的に解放する場合があります。
  • std::shared_ptr :リソースを他の共有ptrと共有します。共有所有権を明示的に解放することができます。
  • std::weak_ptr :私はリソースの所有者ではありませんが、std::weak_ptr::lock メソッドを使用して一時的にリソースの共有所有者になることができます。

このきめの細かい所有権のセマンティックを単なる生のポインターと比較してください。さて、最近の C++ の好きなところはわかりました。

リソース管理のルールの概要は次のとおりです。

  • R.1:リソース ハンドルと RAII (Resource Acquisition Is Initialization) を使用してリソースを自動的に管理する
  • R.2:インターフェースでは、生のポインターを使用して個々のオブジェクトを示します (のみ)
  • R.3:生のポインタ (T* ) は非所有です
  • R.4:生の参照 (T& ) は非所有です
  • R.5:スコープ オブジェクトを優先し、不必要にヒープを割り当てないでください
  • R.6:const 以外を避ける グローバル変数

それぞれについて詳しく見ていきましょう。

R.1:リソース ハンドルと RAII を使用してリソースを自動的に管理する(リソース取得は初期化)

アイデアは非常に単純です。リソースの一種のプロキシ オブジェクトを作成します。プロキシのコンストラクタはリソースを取得し、デストラクタはリソースを解放します。 RAII の重要なアイデアは、C++ ランタイムがローカル オブジェクトの所有者であるということです。

最新の C++ における RAII の 2 つの典型的な例は、スマート ポインターとロックです。スマート ポインターがメモリを管理し、ロックがミューテックスを管理します。

次のクラス ResourceGuard は RAII をモデル化します。

// raii.cpp

#include <iostream>
#include <new>
#include <string>

class ResourceGuard{
 private:
 const std::string resource;
 public:
 ResourceGuard(const std::string& res):resource(res){
 std::cout << "Acquire the " << resource << "." << std::endl;
 }
 ~ResourceGuard(){
 std::cout << "Release the "<< resource << "." << std::endl;
 }
};

int main(){

 std::cout << std::endl;

 ResourceGuard resGuard1{"memoryBlock1"}; // (1)

 std::cout << "\nBefore local scope" << std::endl;
 {
 ResourceGuard resGuard2{"memoryBlock2"}; // (2)
 }
 std::cout << "After local scope" << std::endl;
 
 std::cout << std::endl;

 
 std::cout << "\nBefore try-catch block" << std::endl;
 try{
 ResourceGuard resGuard3{"memoryBlock3"}; // (3)
 throw std::bad_alloc();
 } 
 catch (std::bad_alloc& e){
 std::cout << e.what();
 }
 std::cout << "\nAfter try-catch block" << std::endl;
 
 std::cout << std::endl;

}

ResourceGuard のインスタンスの有効期間が定期的に (1) および (2) 終了するか、不定期に終了するか (3) は違いはありません。 ResourceGuard のデストラクタは常に呼び出されます。これは、リソースが解放されることを意味します。

この例と RAII の詳細について知りたい場合は、私の投稿:ガベージ コレクション - ノー サンクスをお読みください。 Bjarne Stroustrup でさえコメントしました。

R.2:インターフェイスでは、生のポインターを使用して個々のオブジェクトを示します ( )

これは非常にエラーが発生しやすいため、生のポインターは配列を示すべきではありません。これは特に、関数が引数としてポインターを取る場合に当てはまります。

void f(int* p, int n) // n is the number of elements in p[]
{
 // ...
 p[2] = 7; // bad: subscript raw pointer
 // ...
}

配列の間違った側を引数として渡すのは非常に簡単です。

配列の場合、std::vector などのコンテナーがあります。 Standard Template Library のコンテナは排他的所有者です。メモリを自動的に取得および解放します。

R.3:生のポインター (T* ) は非所有です

工場を持っている場合、所有権の問題は特に興味深いものになります。ファクトリは、新しいオブジェクトを返す特別な関数です。今問題です。生のポインター、オブジェクト、std::unique_ptr、または std::shared_ptr を返す必要がありますか?

4 つのバリエーションは次のとおりです。

Widget* makeWidget(int n){ // (1)
 auto p = new Widget{n};
 // ...
 return p;
}

Widget makeWidget(int n){ // (2)
 Widget g{n};
 // ...
 return g;
}

std::unique_ptr<Widget> makeWidget(int n){ // (3)
 auto u = std::make_unique<Widget>(n);
 // ...
 return u;
}

std::shared_ptr<Widget> makeWidget(int n){ // (4)
 auto s = std::make_shared<Widget>(n);
 // ...
 return s;
}

...

auto widget = makeWidget(10);

ウィジェットの所有者は誰ですか?呼び出し元または呼び出し先?例のポインタの質問には答えられないと思います。私も。これは、誰が delete を呼び出すべきかわからないことを意味します。対照的に、ケース (2) から (4) は非常に明白です。オブジェクトまたは std::unique_ptr の場合、呼び出し元が所有者です。 std::shared_ptr の場合、呼び出し元と呼び出し先は所有権を共有します。

1つの質問が残っています。オブジェクトまたはスマートポインターを使用する必要があります。これが私の考えです。

  • ファクトリが仮想コンストラクターなどのポリモーフィックである必要がある場合は、スマート ポインターを使用する必要があります。この特別な使用例については、すでに書いています。詳細については、投稿「C++ コア ガイドライン:コンストラクター (C.50)」を参照してください。
  • オブジェクトを簡単にコピーでき、呼び出し元がウィジェットの所有者でなければならない場合は、オブジェクトを使用します。安価にコピーできない場合は、std::unique_ptr を使用してください。
  • 呼び出し先がウィジェットの有効期間を管理したい場合は、std::shared_ptr を使用してください

R.4:生の参照 (T& ) は非所有です

追加するものは何もありません。生の参照は非所有であり、空にすることはできません。

R.5:スコープ オブジェクトを優先し、不必要にヒープを割り当てないでください

スコープ オブジェクトは、そのスコープを持つオブジェクトです。これは、ローカル オブジェクト、グローバル オブジェクト、またはメンバーの場合があります。 C++ ランタイムがオブジェクトを処理します。メモリの割り当てと割り当て解除は関係なく、std::bad_alloc 例外を取得できません。簡単にするために:可能であれば、スコープ付きオブジェクトを使用してください。

R.6:非 const を避ける グローバル変数

グローバル変数はよくないという話をよく耳にします。それは完全に真実ではありません。非 const グローバル変数は良くありません。非 const グローバル変数を避ける理由はたくさんあります。ここにいくつかの理由があります。簡単にするために、関数またはオブジェクトは非 const グローバル変数を使用すると想定しています。

  • カプセル化 :関数またはオブジェクトは、その範囲外で変更される可能性があります。これは、コードについて考えるのが非常に難しいことを意味します。
  • テスト可能性: 関数を単独でテストすることはできません。関数の効果は、プログラムの状態によって異なります。
  • リファクタリング: 関数を分離して考えることができない場合、コードをリファクタリングすることは非常に困難です。
  • 最適化: 隠れた依存関係が存在する可能性があるため、関数呼び出しを簡単に再配置したり、別のスレッドで関数呼び出しを実行したりすることはできません。
  • 同時実行: データ競合が発生するための必要条件は、共有された変更可能な状態です。 const 以外のグローバル変数は、可変状態で共有されます。

次は?

次の投稿では、非常に重要なリソースであるメモリについて書きます。