Help us understand the problem. What is going on with this article?

VS2015のstd::error_categoryが奇想天外な件について

More than 3 years have passed since last update.

皆さん御存知の通り、C++にはstd::error_categoryという物がある。

win32apiと一緒に使う例
if(/*なんか*/) throw std::system_error(std::error_code(GetLastError(), std::system_category()));

例えばstd::system_category()std::error_categoryから派生したクラス型のオブジェクトへの参照を返す。

おなじみcppjpの解説は

http://cpprefjp.github.io/reference/system_error/system_category.html
error_categoryクラスは、エラー情報を分類するための基本クラスである。

エラーコードから対応するエラーメッセージを取得する処理が異なる場合などで、error_categoryクラスを派生して環境固有のエラー情報を取得するためのクラスを定義できる。

となっている。

まずはデフォルトコンストラクタでも見てみるか

mingw-gcc6.1.0の場合

mingw-gcc6.1.0の場合
namespace std _GLIBCXX_VISIBILITY(default)
{
_GLIBCXX_BEGIN_NAMESPACE_VERSION
  inline namespace _V2 {
  class error_category
  {
  public:
    constexpr error_category() noexcept = default;
  };
  } // end inline namespace
_GLIBCXX_END_NAMESPACE_VERSION
} // namespace

まあそうだよな。

VS2013の場合

VS2013の場合
_STD_BEGIN
class error_category
    {   // categorize an error
public:
    error_category()
        {   // default constructor
        }
    };
_STD_END

VS2013はconstexprに対応していないからまあそうなるわな。

VS2015の場合

VS2015の場合
_STD_BEGIN
class error_category
    {   // categorize an error
public:
    /* constexpr */ error_category() _NOEXCEPT  // TRANSITION
        {   // default constructor
        _Addr = reinterpret_cast<uintptr_t>(this);
        }
    };
_STD_END

あれ、気がついたらいつの間にか開いていた規格書ではconstexprにしろと書いてあるんだけど、なってないな・・・。
・・・ん?_Addrってなんだ。

VS2015の実装を全部見てみる。

VS2015の場合
_STD_BEGIN
class error_category
    {   // categorize an error
public:
    /* constexpr */ error_category() _NOEXCEPT  // TRANSITION
        {   // default constructor
        _Addr = reinterpret_cast<uintptr_t>(this);
        }
    virtual ~error_category() _NOEXCEPT
        {   // destroy the object
        }

    virtual const char *name() const _NOEXCEPT = 0;

    virtual string message(int _Errval) const = 0;

    virtual error_condition
        default_error_condition(int _Errval) const _NOEXCEPT;

    virtual bool equivalent(int _Errval,
        const error_condition& _Cond) const _NOEXCEPT;

    virtual bool equivalent(const error_code& _Code,
        int _Errval) const _NOEXCEPT;

    bool operator==(const error_category& _Right) const _NOEXCEPT
        {   // compare categories for equality
        return (_Addr == _Right._Addr);
        }

    bool operator!=(const error_category& _Right) const _NOEXCEPT
        {   // compare categories for inequality
        return (!(*this == _Right));
        }

    bool operator<(const error_category& _Right) const _NOEXCEPT
        {   // compare categories for order
        return (_Addr < _Right._Addr);
        }

    error_category(const error_category&) = delete;
    error_category& operator=(const error_category&) = delete;

protected:
    uintptr_t _Addr;

    enum : uintptr_t
        {   // imaginary addresses for Standard error_category objects
        _Future_addr = 1,
        _Generic_addr = 3,
        _Iostream_addr = 5,
        _System_addr = 7
        };
    };
_STD_END

どうやら比較演算子で使われているらしい。

VS2015の実装から比較演算子を抜粋
_STD_BEGIN
class error_category
    {   // categorize an error
    bool operator==(const error_category& _Right) const _NOEXCEPT
        {   // compare categories for equality
        return (_Addr == _Right._Addr);
        }
    bool operator<(const error_category& _Right) const _NOEXCEPT
        {   // compare categories for order
        return (_Addr < _Right._Addr);
        }
    };
_STD_END

ちょっとまって、規格書を見に行こう。

http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2014/n4296.pdf

§ 19.5.1.1

19.5.1.1 Class error_category overview [syserr.errcat.overview]

  1. The class error_category serves as a base class for types used to identify the source and encoding of a particular category of error code. Classes may be derived from error_category to support categories of errors in addition to those defined in this International Standard. Such classes shall behave as specified in this subclause. [ Note: error_category objects are passed by reference, and two such objects are equal if they have the same address. This means that applications using custom error_category types should create a single object of each such type. —end note ]

§ 19.5.1.3

2

bool operator==(const error_category& rhs) const noexcept;

Returns: this == &rhs.

4

bool operator<(const error_category& rhs) const noexcept;

Returns: less<const error_category*>()(this, &rhs).
[ Note: less (20.9.6) provides a total ordering for pointers. —end note ]

デフォルトコンストラクタはthisポインタを保存しているみたいだけど・・・なんでそんなことを?

std::system_categoryを調べてみる

この謎をとくために、std::system_categoryという関数を見てみよう。

_STD_BEGIN
template<class _Ty> inline
    _Ty& _Immortalize()
    {   // return a reference to an object that will live forever
    /* MAGIC */ static _Immortalizer<_Ty> _Static;
    return (*reinterpret_cast<_Ty *>(&_Static._Storage));
    }
inline const error_category& system_category() _NOEXCEPT
    {   // get system_category
    return (_Immortalize<_System_error_category>());
    }
_STD_END

_Immortalizeは規格書の要求を満たすためにstatic storageにクラスを構築するためのものなんだろう。(よく見たら微妙に違うみたいだけど)

_STD_BEGIN
        // CLASS _System_error_category
class _System_error_category
    : public error_category
    {   // categorize an operating system error
public:
    _System_error_category() _NOEXCEPT
        {   // default constructor
        _Addr = _System_addr;
        }

    virtual const char *name() const _NOEXCEPT
        {   // get name of category
        return ("system");
        }

    virtual string message(int _Errcode) const
        {   // convert to name of error
        const unsigned long _Size = 32767;
        string _Narrow(_Size, '\0');

        const unsigned long _Val = _Winerror_message(
            static_cast<unsigned long>(_Errcode), &_Narrow[0], _Size);

        if (_Val == 0)
            return ("unknown error");

        _Narrow.resize(_Val);
        _Narrow.shrink_to_fit();
        return (_Narrow);
        }

    virtual error_condition
        default_error_condition(int _Errval) const _NOEXCEPT
        {   // make error_condition for error code (generic if possible)
        int _Posv = _Winerror_map(_Errval);

        if (_Posv != 0)
            return (error_condition(_Posv, generic_category()));
        else
            return (error_condition(_Errval, system_category()));
        }
    };
_STD_END

ん!?なんかデフォルトコンストラクタがおかしいぞ・・・?

_STD_BEGIN
class _System_error_category
    : public error_category
    {   // categorize an operating system error
    _System_error_category() _NOEXCEPT
        {   // default constructor
        _Addr = _System_addr;
        }
_STD_END

_System_addrってなんだ?

・・・そういえばstd::error_categoryの実装を載せた時にへんなenumがあったな

_STD_BEGIN
class error_category
    {   // categorize an error
protected:
    enum : uintptr_t
        {   // imaginary addresses for Standard error_category objects
        _Future_addr = 1,
        _Generic_addr = 3,
        _Iostream_addr = 5,
        _System_addr = 7
        };
    };
_STD_END

あった。ご丁寧にコメントが書いてある。

imaginary addresses for Standard error_category objects

Google先生お願いします。

標準error_categoryオブジェクトの架空のアドレス

ほう・・・。つまり、std::error_categoryクラスの比較演算子は、このenumによって作られた仮想的なアドレスを比較していると。
・・・いやそれってだめなんじゃ?もう一度規格書をみるよ。

§ 19.5.1.1

19.5.1.1 Class error_category overview [syserr.errcat.overview]

  1. The class error_category serves as a base class for types used to identify the source and encoding of a particular category of error code. Classes may be derived from error_category to support categories of errors in addition to those defined in this International Standard. Such classes shall behave as specified in this subclause. [ Note: error_category objects are passed by reference, and two such objects are equal if they have the same address. This means that applications using custom error_category types should create a single object of each such type. —end note ]

ここでいうaddressはつまるところポインタのことじゃないのか?

cppjpでの @faithandbrave@github さんの解説も

http://cpprefjp.github.io/reference/system_error/error_category/op_less.html

bool operator<(const error_category& rhs) const noexcept;

error_categoryオブジェクトのポインタの小なり比較を行う。

自身のポインタがrhsオブジェクトへのポインタより小さい場合trueを返し、そうでなければfalseを返す。

と書いている。

結論

VS2015は、それが規格的にいいのかは知らんが、std::error_categoryの比較演算子の実装に仮想的なアドレスを使っている。

このせいで自分でstd::error_categoryの派生クラスを作るときに困る。

皆さんに聞きたい、この実装ってありなの?

追記:VS2015で仕様が変わった理由らしきもの

ん!?

STL Fixes In VS 2015, Part 2 | Visual C++ Team Blog

https://blogs.msdn.microsoft.com/vcblog/2015/07/14/stl-fixes-in-vs-2015-part-2/
* Error category objects didn’t behave properly across different DLLs/EXEs (DevDiv#666062, DevDiv#1095970/Connect#1053790). The tale of woe here was complicated. Calling generic_category(), for example, is supposed to return a reference to a single unique object, regardless of where it’s called. This is usually achieved by separate compilation into the STL’s DLL (or static LIB). However, we can’t separately compile error_category machinery, because it has a virtual message() returning std::string, whose representation is affected by ITERATOR_DEBUG_LEVEL. So, generic_category() is implemented header-only – but that means that different user DLLs end up with different instantiations and therefore different objects. (It’s also possible for this to cause trouble between a user’s EXE and the STL’s DLL.) We fixed this to achieve as much conformance as possible. We’ve taught error_category, its derived classes, and its operator==()/operator!=() to consider all genericcategory() objects to be equal, even if they live at different addresses in different DLLs (and similarly for the other error category objects in the Standard). This has been implemented so that user-defined error category objects will be unaffected. The only thing we can’t fix is a direct comparison of error_category addresses (code should use operator==() instead).

[C++] Comparison std::error_code from std::system_error to std::errc enum value incorrect when using DLL-version of run-time library

https://connect.microsoft.com/VisualStudio/feedback/details/1053790
A comparison of the error code from an std::system_error object thrown by std::thread to std::errc::resource_unavailable_try_again appears to produce an incorrect Boolean result when the application uses a DLL-specific version of the run-time library (compiler option "/MD" or "MDd").

The issue can be reproduces by the attached file, "MyTest.cpp", or by the online C++ compiler at http://rextester.com/EMVQXX20044

Christopher Kohlhoff encouraged me to submit this bug report. He suggested that the issue might be caused by the fact that the implementation compares error categories by their address, and from MSVC

うーん、英語が得意じゃない私にはちときつい・・・。
dllにしたところ、アドレスがdll使用側と一致しないぞい、ってこと?(あやふや)

yumetodo
ありきたりなC++erです。最近C++書いていません(あれっ
http://yumetodo.hateblo.jp/
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした