🐙

std::error_codeとstd::error_condition

2023/11/11に公開

概要

自分で定義したエラーコードをstd::error_codeとして扱うためのアンチョコと、異なるエラーカテゴリに属するエラー値だが同じ意味合いとして扱いたくなったときのstd::error_conditionの使い方メモです.
ハードウェア(とSDK)を切り替えながら動くソフトウェアを書くときに少し役に立つかもしれません.

std::error_code

std::error_codeを使うことで、型消去した状態でエラーを伝搬させることが出来るようになります. また、エラー内容をログファイルに残すときなどに統一的な方法を取ることが出来るようになります.

std::error_codeのアンチョコ

test.h

#pragma once
#include <system_error>

namespace test {
    enum class errc
    {
        // 0は成功として扱われるので、それ以外の数値から始める
        bad_request = 1,
        timeout,
    };

    // 定義したエラーコードと同じ名前空間に定義
    std::error_code make_error_code(errc);
}

namespace std
{
    template <>
    struct is_error_code_enum<test::errc> : true_type {};
}

自分で定義したエラーコードをstd::error_codeとして扱うための実装は、以下のようにして閉じ込めることが出来ます.
test.cpp

#include "test.h"

namespace {
    class test_errc_category : public std::error_category
    {
    public:
        virtual const char* name() const noexcept override final { return "test_errc_category"; }

        virtual std::string message(int c) const override final
        {
            switch (static_cast<test::errc>(c))
            {
            case test::errc::bad_request: return "bad request";
            case test::errc::timeout:     return "timeout";
            default:
                return "unknown";
            }
        }
    }; // class test_errc_category

    const test_errc_category theErrorCategory;
}


namespace test {
    std::error_code make_error_code(errc e)
    {
        return { static_cast<int>(e), theErrorCategory };
    }
}

使い方

#include "test.h"
#include <cassert>
#include <iostream>

std::error_code some_func()
{
    return test::errc::bad_request;
}

int main()
{
    const std::error_code ec = some_func();
    std::cout << "category: " << ec.category().name() << "\n";
    std::cout << "message: " << ec.message() << "\n";
    std::cout << "value: " << ec.value() << "\n";
    
    const std::error_code eperm = std::error_code(static_cast<int>(std::errc::operation_not_permitted), std::generic_category());
    assert(eperm.value() == 1);
    assert(ec != eperm);
    return 0;
}

以下のように出力されます.

category: test_errc_category
message: bad request
value: 1

std::error_codeとして扱えるようにしておくことで、カテゴリー・エラーメッセージ・エラー値を統一的な方法で記録することが出来るようになります. 例えば、上記コードにあるsome_functionが異なるカテゴリーのエラー(std::errc::operation_not_permittedなど)を返すように実装が切り替わったとしても記録する部分のコードは同じままです.

エラーコードを見て対処したい時の問題

どんなカテゴリーのエラーコードが返ってくるか分からないとき

あきらめるしかなさそうです...

特定カテゴリーのエラーコードが返ってくると分かっているとき

以下のようなコードで対処できます.

const std::error_code ec = func();
if (ec == test::errc::bad_request) {
    // ちゃんとリクエストして!!
}
else if (ec == test::errc::timeout) {
    // 時間かかり過ぎ!!
}
else {
    // ここではどうにもならないので呼び出し元に託す...
}

複数カテゴリーのエラーコードが返ってくると分かっているとき

例えば2つのライブラリを組み合わせて実装する必要がある関数.

const std::error_code ec = func(request);
if (ec == test::errc::bad_request) {
    // ちゃんとリクエストして!!
}
else if (ec == test::errc::timeout) {
    // 時間かかり過ぎ!!
}
else if (ec == another_lib::errc::bad_request) {
    // ちゃんとリクエストして??
}
else if (ec == another_lib::errc::too_busy) {
    // また後で
}
else {
    // ここではどうにもならないので呼び出し元に託す...
}

エラーの内容(エラーへの対処方法)が異なるものに対してはif文を並べるような方法でも良さそうですが、同じ内容のものに対してもif文を並べてしまうのはあまりスジが良くなさそうです. 例えば、さらに別のライブラリを使用したり(あるいは切り替えた)ときに、別カテゴリーのbad_requestに対するif文を追加し忘れたりするかもしれません.

エラーのカテゴリや値が異なっても意味がならまとめて扱いたい場合は、std::error_conditionで対応することが出来ます.

std::error_conditionアンチョコ

my_lib.h

#pragma once
#include <system_error>

namespace my_lib {
    enum class err_cond
    {
        bad_request = 1,
        some_error
    };
    // 同じ名前空間に定義
    std::error_condition make_error_condition(err_cond);
}

namespace std
{
    template <>
    struct is_error_condition_enum<my_lib::errc> : true_type {};
}
// make_error_codeの実装, is_error_code_enumの特殊化をしてしまうと比較時などでエラーになります

my_lib.cpp

#include "my_lib.h"
#include "test.h"
#include "another_lib.h"

namespace {
    class my_lib_error_category : public std::error_category
    {
    public:
        virtual const char* name() const noexcept override final { return "my_lib_error_category"; }
        virtual std::string message(int c) const override final
        {
            // 省略
        }

        // この関数をオーバーライドする
        bool equivalent(const std::error_code& ec, int condition) const noexcept override
        {
	    // もし、カテゴリが欲しいならこうやって取得する
            //const std::error_category& testCat = std::error_code{ lib1_errc{} }.category();
            const std::error_category& anotherLibCat = std::error_code{ another_lib::errc{} }.category();

            switch (static_cast<my_lib::err_cond>(condition))
            {
            case my_lib::err_cond::bad_request:
                if (ec == test::errc::bad_request) {
                    return true;
                }
		if (ec == another_lib::errc::bad_request) {
		    return true;
		}
                return false;
            case my_lib::errc::some_error:
                if (ec.category() == anotherLibCat) {
                    return 100 <= ec.value() && << ec.value() < 1000;
                }
                return false;
            default:
                return false;
            }
        }
    }; // class my_lib_errc_category

    const my_lib_error_category theErrorCategory;
}


namespace my_lib {
    std::error_condition make_error_condition(err_cond e)
    {
        return { static_cast<int>(e), theErrorCategory };
    }
}

アンチョコ解説

bool equivalent(const std::error_code& ec, int condition) const noexcept関数をオーバーライドすることで、同じ意味合いとして扱いたいエラーコードを指定することができます.

equivalent関数の引数であるconditionには、my_lib::err_condとして自身が定義した「同じ意味合い」が渡されます. 一方、ec引数には「同じ意味合い」かどうかを調べる対象が渡されます. (test::errc::bad_requestなどです)

conditionで指定されたものと同じ意味合いで扱いたいerror_codeの場合にはtrueを返し、それ以外にはfalseを返すように実装します.

case my_lib::errc::some_error:の箇所のように、あるエラーカテゴリであれば3桁のコードは同じ意味合いとして扱う、といった表現をすることもできます.

使い方

main.cpp

#include "test.h"
#include "another_lib.h"
#include "my_lib.h"

#include <cassert>
#include <iostream>


int main()
{
    std::error_code ec1, ec2;

    ec1 = test::errc::bad_request;
    ec2 = another_lib::bad_request;
    assert(ec1 == my_lib::err_cond::bad_request);
    assert(ec2 == my_lib::err_cond::bad_request);
    
    // my_lib::err_cond を介して「同じ意味合い」ではあるが、同じものではない
    assert(ec1 != ec2);
    
    
    const std::error_code ec = func(request);
    if (ec == my_lib::err_cond::bad_request) {
        // まとめて扱えるようになった
    }
    //else if (ec == another_lib::errc::bad_request) {} // 不要になった
    // 個別カテゴリのエラーコードとしての扱いは変わらずできる
    else if (ec == test::errc::timeout) {}
    else if (ec == another_lib::errc::too_busy) {}
    else {}
    
    return 0;
}

参考にしたサイト

https://akrzemi1.wordpress.com/2017/08/12/your-own-error-condition/
https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2018/p0824r1.html

Discussion