🐥

RAII(Resource Acquisition Is Initialization)++

に公開

今週少し悩んでいた問題について書いてみましょう。以下のような何かをマウントするクラスがあるとしましょう。

   class MyMount {
   public:
      MyMount();

      int Mount(const std::string& mountName);
      int Unmount();

   private:
      std::string m_mountName;
   };

Mount()Unmount()もエラーになる可能性があり、エラーになった場合はエラーを出力して、現在の関数を中断しないといけないです。
しかも、以下のようにMount()をコールしてからUnmount()がコールされるまで、doSomething1()doSomething2()というエラーになりえる他の関数をコールしているため、これらがエラーになった場合もUnmount()をコールしないといけないです。

そのまま書いたら、以下のようになると思います。

int DoSomethingWithMount(const std::string& mountName) {
   MyMount myMount;
   int ret = myMount.Mount(mountName);
   if (ret < 0) {
      std::cout << "MyMount::Mount() failed with " << ret << std::endl;
      return ret;
   }

   ret = doSomething1();
   if (ret < 0) {
      std::cout << "doSomething1() failed with " << ret << std::endl;
      const int umountRet = myMount.Unmount();
      if (umountRet < 0) {
         std::cout << "MyMount::Unmount() failed with " << umountRet << std::endl;
      }
      return ret;
   }

   ret = doSomething2();
   if (ret < 0) {
      std::cout << "doSomething2() failed with " << ret << std::endl;
      const int umountRet = myMount.Unmount();
      if (umountRet < 0) {
         std::cout << "MyMount::Unmount() failed with " << umountRet << std::endl;
      }
      return ret;
   }

   ret = myMount.Unmount();
   if (ret < 0) {
      std::cout << "MyMount::Unmount() failed with " << ret << std::endl;
      return ret;
   }

   return 0;
}

エラーなった後のUmount()がエラーになっても、元のエラーを返すようにしています。

これだと、Umount()をコールして戻り値を確認するコードが何度も複製されてしまいます。

goto

古い C++ だったら以下のようにgotoを使うと思います。

int DoSomethingWithMount(const std::string& mountName) {
   MyMount myMount;
   int ret = myMount.Mount(mountName);
   if (ret < 0) {
      std::cout << "MyMount::Mount() failed with " << ret << std::endl;
      return ret;
   }

   ret = doSomething1();
   if (ret < 0) {
      std::cout << "doSomething1() failed with " << ret << std::endl;
      goto Unmount;
   }

   ret = doSomething2();
   if (ret < 0) {
      std::cout << "doSomething2() failed with " << ret << std::endl;
      goto Unmount;
   }

Unmount:
   ret = myMount.Unmount();
   if (ret < 0) {
      std::cout << "MyMount::Unmount() failed with " << ret << std::endl;
      return ret;
   }

   return 0;
}

そもそも確保したリソースを明示的に解放する関数があるクラス自体がかなり古いデザインで、gotoと相性が良いです。
正直悪くはないですが、gotoはできれば使いたくないため、他の方法を考えましょう。

ラムダ式関数

doSomething1()doSomething2()をラムダ式関数に入れとどうなるでしょうか。

int DoSomethingWithMount(const std::string& mountName) {
   MyMount myMount;
   int ret = myMount.Mount(mountName);
   if (ret < 0) {
      std::cout << "MyMount::Mount() failed with " << ret << std::endl;
   }

   auto doStuff([&] {
      int ret = doSomething1();
      if (ret < 0) {
         std::cout << "doSomething1() failed with " << ret << std::endl;
         return ret;
      }

      ret = doSomething2();
      if (ret < 0) {
         std::cout << "doSomething2() failed with " << ret << std::endl;
         return ret;
      }
   });

   ret = doStuff();
   const int unmountRet = myMount.Unmount();
   if (ret < 0) {
      std::cout << "doStuff failed with " << ret << std::endl;
      if (unmountRet < 0) {
         std::cout << "MyMount::Unmount() failed with " << unmountRet << std::endl;
      }
      return ret;
   }
   else if (unmountRet < 0) {
      std::cout << "MyMount::Unmount() failed with " << unmountRet << std::endl;
      return unmountRet;
   }

   return 0;
}

doSomething1()doSomething2()のコールをラムダ式関数に詰めることで、これらのエラーケースを1つにまとめることができます。これでコードの複製は殆ど発生しなくなります。
でもラムダ式関数を追加することで、関数全体の処理が複雑になっています。特にラムダ式関数に慣れていない人にとって分かりづらくなってしまっているかもしれないです。

デストラクタ

ここで一旦初心に帰って、問題を一から考え直してみました。新しい C++ ではRAII (Resource Acquisition Is Initialization)という原則があって、本当はMount()のようなリソースの確保はコンストラクタで行って、Unmount()のようなリソースの開放はデストラクタの中で行うべきです。そうすると、MyMountのインスタンスを作る時に自動的にMount()がコールされて、インスタンスが破棄される時に自動的にUnmount()がコールされるようになります。

でも1つだけ問題があります。コンストラクタとデストラクタには戻り値がないため、コンストラクタの中でMount()がエラーになった場合と、デストラクタの中でUnmount()がエラーになった場合は、以下のようにエラーは出力できますが、エラーコードを返せないです。

class MyMount {
public:
   MyMount(const std::string& mountName)
   : m_mountName(mountName)
   {
      const int ret = Mount();
      if (ret < 0) {
         std::cout << "MyMount::Unmount failed with " << ret << std::endl;
         // コンストラクタには戻り値がないため。エラーを返せないです。
      }
   }
   ~MyMount()
   {
      const int ret = Unmount();
      if (ret < 0) {
         std::cout << "MyMount::Unmount failed with " << ret << std::endl;
         // デストラクタには戻り値がないため。エラーを返せないです。
      }
   }

   int Mount();

private:
   std::string m_mountName;
};

コンストラクタの場合は、例外を投げて、呼び出し元でキャッチすれば良いだけの話ですが、デストラクタの中で例外を投げて、その例外が外に漏れるとstd::terminate()がコールされ、プログラムがクラッシュします。
後、例外が使えない環境も存在します。

ここで色々考えて、今のところは以下の方法にたどり着きました。

class MyMount {
public:
   MyMount(const std::string& mountName, int& unmountResult)
   : m_mountName(mountName)
   , m_unmountResult(unmountResult)
   {}

   ~MyMount()
   {
      m_unmountResult  = Unmount();
      if (m_unmountResult < 0) {
         std::cout << "MyMount::Unmount failed with " << m_unmountResult << std::endl;
      }
   }

   int Mount();

private:
   std::string m_mountName;
   int& m_unmountResult;
};

int DoSomethingWithMount(const std::string& mountName) {
   int unmountResult = 0;
   {
      MyMount myMount(mountName, unmountResult);
      int ret = myMount.Mount();
      if (ret < 0) {
         std::cout << "MyMount::Mount failed with " << ret << std::endl;
         return ret;  // ここでmyMount変数のスコープが終わりデストラクタが実行される
      }

      ret = doSomething1();
      if (ret < 0) {
         std::cout << "doSomething1() failed with " << ret << std::endl;
         return ret;  // ここでmyMount変数のスコープが終わりデストラクタが実行される
      }

      ret = doSomething2();
      if (ret < 0) {
         std::cout << "doSomething2() failed with " << ret << std::endl;
         return ret;  // ここでmyMount変数のスコープが終わりデストラクタが実行される
      }
      // ここでmyMount変数のスコープが終わりデストラクタが実行される
   }
   if (unmountResult < 0) {
      std::cout << "MyMount::Unmount failed with " << unmountResult << std::endl;
      return unmountResult;
   }
   
   return 0;
}

MyMountUnmount()の結果用の変数を渡して、MyMountのデストラクタが発動した後に、その変数で成功したかを判断します。後、スコープで囲むことで、MyMountのデストラクタを好きなタイミングで発動させられます。
この方法はどうなのでしょうかね… まだ少し腑に落ちないところもありますが、とりあえずgotoとラムダ式関数よりはいいかもしれないです。

本当はUnmount()が失敗することがあるのはいけないのです。新しい C++ ではデストラクタやデストラクタの中で実行されるようなクリーンアップ処理は絶対に失敗しないような作りになっていないといけないのです。
この問題の本当の原因はエラーを返すUnmount()とデストラクタの相性が良くないことですね。もしこれよりいい解決方法が知っている人がいれば、是非教えてください。


|cpp記事一覧へのリンク|

Discussion