例外的なコードと非例外的なコードをインターフェイスで連結する方法

ここでは、C++ コードで一貫した例外処理を実装する方法と、例外の境界でエラー コードとの間でそれらの例外を変換する方法を説明します。

C++ コードは、例外を使用しないコード (非例外コード) とのやり取りが必要な場合があります。 そのようなインターフェイスは例外境界と呼ばれます。 たとえば、C++ プログラム中で Win32 関数 CreateFile を呼び出すことが必要な場合があります。 CreateFile は例外をスローしません。 GetLastError 関数で取得できるエラー コードを設定します。 作成する C++ プログラムが重要である場合、一貫した例外ベースのエラー処理ポリシーを適用するのが一般的です。 また、非例外コードとやり取りするというだけの理由から例外の使用をあきらめたくないでしょう。 さらに、C++ コードに例外ベースのエラー ポリシーと非例外ベースのエラー ポリシーを混在させたくありません。

C++ からの非例外関数の呼び出し

C++ から非例外関数を呼び出す場合、すべてのエラーを検出して例外をスローする C ++ 関数でその関数をラップします。 そのようなラッパー関数を設計するときは、まず提供する例外保証の種類 (noexcept、strong、basic) を決定します。 次に、例外がスローされるときにすべてのリソース (たとえばファイル ハンドル) が正しく解放されるように関数を設計します。 通常は、スマート ポインターなどのリソース マネージャー使用してリソースを所有することになります。 設計に関する考慮事項の詳細については、「方法: 例外安全性に対応した設計をする」を参照してください。

次の例は、内部的に Win32 関数 CreateFileReadFile を使用して 2 個のファイルを開いて読み取る C++ 関数を示しています。 File クラスは、ファイル ハンドルの RAII (Resource Acquisition Is Initialization) ラッパーです。 そのコンストラクターは、"ファイルが見つからない" 状態を検出し、例外をスローして、C++ 実行可能ファイルの呼び出し履歴の上方向 (この例では main() 関数) にエラーを伝達します。 File オブジェクトが完全に作成された後で例外がスローされる場合、デストラクターは自動的に CloseHandle を呼び出してファイル ハンドルを解放します (好みに応じて、この同じ目的のために Active Template Library (ATL) の CHandle クラスか、unique_ptr とカスタム削除関数を使用することもできます)。 Win32 と CRT の API を呼び出す関数は、エラーを検出し、ローカルに定義された ThrowLastErrorIf 関数を使用して C++ の例外をスローします。そこで runtime_error クラスから派生した Win32Exception クラスが使用されます。 この例のすべての関数は、strong 例外保証を提供します。つまり、これらの関数のどの場所で例外がスローされても、リソースがリークせず、プログラムの状態も変更されません。

// compile with: /EHsc
#include <Windows.h>
#include <stdlib.h>
#include <vector>
#include <iostream>
#include <string>
#include <limits>
#include <stdexcept>

using namespace std;

string FormatErrorMessage(DWORD error, const string& msg)
{
    static const int BUFFERLENGTH = 1024;
    vector<char> buf(BUFFERLENGTH);
    FormatMessageA(FORMAT_MESSAGE_FROM_SYSTEM, 0, error, 0, buf.data(),
        BUFFERLENGTH - 1, 0);
    return string(buf.data()) + "   ("  + msg  + ")";
}

class Win32Exception : public runtime_error
{
private:
    DWORD m_error;
public:
    Win32Exception(DWORD error, const string& msg)
        : runtime_error(FormatErrorMessage(error, msg)), m_error(error) { }

    DWORD GetErrorCode() const { return m_error; }
};

void ThrowLastErrorIf(bool expression, const string& msg)
{
    if (expression) {
        throw Win32Exception(GetLastError(), msg);
    }
}

class File
{
private:
    HANDLE m_handle;

    // Declared but not defined, to avoid double closing.
    File& operator=(const File&);
    File(const File&);
public:
    explicit File(const string& filename)
    {
        m_handle = CreateFileA(filename.c_str(), GENERIC_READ, FILE_SHARE_READ,
            nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_READONLY, nullptr);
        ThrowLastErrorIf(m_handle == INVALID_HANDLE_VALUE,
            "CreateFile call failed on file named " + filename);
    }

    ~File() { CloseHandle(m_handle); }

    HANDLE GetHandle() { return m_handle; }
};

size_t GetFileSizeSafe(const string& filename)
{
    File fobj(filename);
    LARGE_INTEGER filesize;

    BOOL result = GetFileSizeEx(fobj.GetHandle(), &filesize);
    ThrowLastErrorIf(result == FALSE, "GetFileSizeEx failed: " + filename);

    if (filesize.QuadPart < (numeric_limits<size_t>::max)()) {
        return filesize.QuadPart;
    } else {
        throw;
    }
}

vector<char> ReadFileVector(const string& filename)
{
    File fobj(filename);
    size_t filesize = GetFileSizeSafe(filename);
    DWORD bytesRead = 0;

    vector<char> readbuffer(filesize);

    BOOL result = ReadFile(fobj.GetHandle(), readbuffer.data(), readbuffer.size(),
        &bytesRead, nullptr);
    ThrowLastErrorIf(result == FALSE, "ReadFile failed: " + filename);

    cout << filename << " file size: " << filesize << ", bytesRead: "
        << bytesRead << endl;

    return readbuffer;
}

bool IsFileDiff(const string& filename1, const string& filename2)
{
    return ReadFileVector(filename1) != ReadFileVector(filename2);
}

#include <iomanip>

int main ( int argc, char* argv[] )
{
    string filename1("file1.txt");
    string filename2("file2.txt");

    try
    {
        if(argc > 2) {
            filename1 = argv[1];
            filename2 = argv[2];
        }

        cout << "Using file names " << filename1 << " and " << filename2 << endl;

        if (IsFileDiff(filename1, filename2)) {
            cout << "+++ Files are different." << endl;
        } else {
            cout<< "=== Files match." << endl;
        }
    }
    catch(const Win32Exception& e)
    {
        ios state(nullptr);
        state.copyfmt(cout);
        cout << e.what() << endl;
        cout << "Error code: 0x" << hex << uppercase << setw(8) << setfill('0')
            << e.GetErrorCode() << endl;
        cout.copyfmt(state); // restore previous formatting
    }
}

非例外コードからの例外コードの呼び出し

extern "C" と宣言された C++ の関数は C プログラムから呼び出すことができます。 C++ COM サーバーは、さまざまな言語で記述されたコードで使用できます。 非例外コードから呼び出される、例外に対応した C++ のパブリック関数を実装する場合、C++ の関数は、例外が呼び出し元に伝達されないようにする必要があります。 このような呼び出し元は、C++ 例外をキャッチまたは処理することはできません。 プログラムが終了したり、リソースをリークしたり、未定義の動作が発生したりする可能性があります。

extern "C" C++ の関数は、対処方法がわかっているすべての例外を明示的にキャッチし、呼び出し元が理解できるエラー コードに適宜変換することをお勧めします。 発生する可能性があるすべての例外が不明な場合は、C++ の関数の最後のハンドラーとして catch(...) ブロックを記述します。 その場合、プログラムが不明な、かつ回復不能な状態になる可能性があるため、呼び出し元に致命的なエラーを報告することをお勧めします。

次の例に示す関数では、スローされる例外が、Win32Exception か、std::exception から派生した例外の種類であることを仮定しています。 関数は、これらの種類の例外をすべてキャッチし、Win32 エラー コードとして呼び出し元にエラー情報を伝達します。

BOOL DiffFiles2(const string& file1, const string& file2)
{
    try
    {
        File f1(file1);
        File f2(file2);
        if (IsTextFileDiff(f1, f2))
        {
            SetLastError(MY_APPLICATION_ERROR_FILE_MISMATCH);
            return FALSE;
        }
        return TRUE;
    }
    catch(Win32Exception& e)
    {
        SetLastError(e.GetErrorCode());
    }

    catch(std::exception& e)
    {
        SetLastError(MY_APPLICATION_GENERAL_ERROR);
    }
    return FALSE;
}

例外からエラー コードに変換するとき、考えられる 1 つの問題は、例外に格納できるような豊富な情報がエラー コードに含まれていないことが多いということです。 この問題を解決するには、スローされる可能性がある特定の種類の例外ごとに catch ブロックを記述し、ログを出力して、エラー コードに変換する前に例外の詳細を記録します。 この方法により、複数の関数すべてが同じ catch ブロックのセットを使用する場合、繰り返しコードが作成されます。 コードの繰り返しを避けるための良い方法の 1 つは、try ブロックと catch ブロックを実装し、try ブロックの中で呼び出される関数オブジェクトを受け取る、1 つのプライベート ユーティリティ関数にこれらのブロックをリファクタリングすることです。 各パブリック関数では、ラムダ式としてユーティリティ関数にコードを渡します。

template<typename Func>
bool Win32ExceptionBoundary(Func&& f)
{
    try
    {
        return f();
    }
    catch(Win32Exception& e)
    {
        SetLastError(e.GetErrorCode());
    }
    catch(const std::exception& e)
    {
        SetLastError(MY_APPLICATION_GENERAL_ERROR);
    }
    return false;
}

次の例は、ファンクタを定義するラムダ式を記述する方法を示しています。 多くの場合、ラムダ式は、名前付き関数オブジェクトを呼び出すコードよりもインラインで読みやすくなります。

bool DiffFiles3(const string& file1, const string& file2)
{
    return Win32ExceptionBoundary([&]() -> bool
    {
        File f1(file1);
        File f2(file2);
        if (IsTextFileDiff(f1, f2))
        {
            SetLastError(MY_APPLICATION_ERROR_FILE_MISMATCH);
            return false;
        }
        return true;
    });
}

ラムダ式について詳しくは、「ラムダ式」をご覧ください。

例外コードから非例外コードを使用した例外コードの呼び出し

例外を認識しないコード全体で例外をスローすることは可能ですが、推奨されません。 たとえば、C++ プログラムでは、指定したコールバック関数を使用するライブラリを呼び出すことができます。 場合によっては、元の呼び出し元が処理できる非例外コードで、コールバック関数から例外をスローすることができます。 ただし、例外が正常に機能する状況は厳格です。 スタック アンワインド セマンティクスを保持するように、ライブラリ コードをコンパイルする必要があります。 例外を認識しないコードは、C++ 例外をトラップする可能性のある処理を行うことはできません。 また、呼び出し元とコールバックの間のライブラリ コードは、ローカル リソースを割り当てることができません。 たとえば、例外を認識しないコードでは、割り当てられたヒープ メモリを指すローカルを持つことはできません。 これらのリソースは、スタックがアンワインドされるときにリークします。

これらの要件は、例外を認識しないコードで例外をスローするために満たされる必要があります。

  • /EHs を使用し、例外を認識しないコードでコード パス全体をビルドできます。
  • スタックのアンワインド時にリークする可能性のある、ローカルに割り当てられたリソースはありません。
  • このコードには、すべての例外をキャッチする __except 構造化例外ハンドラーがありません。

非例外コードで例外をスローするとエラーが発生しやすくなり、デバッグが困難になる可能性があるため、推奨されません。

関連項目

例外とエラー処理に関する最新の C++ のベスト プラクティス
方法: 例外安全性に対応した設計