ボレロ村上 - ENiyGmaA Code

中3女子です。

コンパイル時/実行時両用アサート

中3女子です。


今回は、アサーションについて。
通常 C++ において、実行時チェックは assert、コンパイル時チェックは static_assert によって行われる。
しかしながら、これらは、constexpr 関数の中で用いることはできない。


もちろん、assert は定数式ではないから、使えない。
また、constexpr 関数の引数は、その時点でコンパイル時定数ではないから、static_assert で使うことはできない。

template<typename T>
constexpr T
div(T num, T denom) {
    static_assert(denom != 0, "divide by zero"); /* error! */
    return num / denom;
}


このため今回は、constexpr 関数の中で使うことができるアサーションを考える。
なお、技術的着想は RiSK氏のブログ(constexpr な関数・クラスでのエラーハンドリング - とくにあぶなくないRiSKのブログ)から、
インタフェースはBoost.Assert(Boost: assert.hpp documentation - 1.53.0)を参考にした。
有用な技術情報を公開なさっている方々に感謝いたします。

Sprout.Assert

そのような機能を既に Sprout で実装してあるから、そのコードと用法を見ることにする。

#ifndef SPROUT_ASEERT_HPP
#define SPROUT_ASEERT_HPP

#if defined(SPROUT_DISABLE_ASSERTS) || defined(NDEBUG)
#   include <sprout/config.hpp>
#elif defined(SPROUT_ENABLE_ASSERT_HANDLER)
#   include <sprout/config.hpp>
#else
#   include <cstdlib>
#   include <iostream>
#   include <sprout/config.hpp>
#endif
#if !(defined(SPROUT_DISABLE_ASSERTS) || defined(NDEBUG))
#   include <sprout/preprocessor/stringize.hpp>
#endif

#if !(defined(SPROUT_DISABLE_ASSERTS) || defined(NDEBUG))
    //
    // SPROUT_ASSERTION_FAILED_FORMAT
    //
#   ifndef SPROUT_ASSERTION_FAILED_FORMAT
#       define SPROUT_ASSERTION_FAILED_FORMAT(expr, file, line) \
            "***** Internal Program Error - assertion (" #expr ") failed: " file "(" SPROUT_PP_STRINGIZE(line) ")"
#   endif
#endif

//
// SPROUT_ASSERT
//

#if defined(SPROUT_DISABLE_ASSERTS) || defined(NDEBUG)

#   define SPROUT_ASSERT(expr) \
        ((void)0)

#elif defined(SPROUT_ENABLE_ASSERT_HANDLER)

namespace sprout {
    //
    // assertion_info
    //
    class assertion_info {
    private:
        char const* expr_;
        char const* function_;
        char const* file_;
        long line_;
    public:
        SPROUT_CONSTEXPR assertion_info(char const* expr, char const* function, char const* file, long line)
            : expr_(expr), function_(function), file_(file), line_(line)
        {}
        SPROUT_CONSTEXPR char const* expr() const SPROUT_NOEXCEPT {
            return expr_;
        }
        SPROUT_CONSTEXPR char const* function() const SPROUT_NOEXCEPT {
            return function_;
        }
        SPROUT_CONSTEXPR char const* file() const SPROUT_NOEXCEPT {
            return file_;
        }
        SPROUT_CONSTEXPR long line() const SPROUT_NOEXCEPT {
            return line_;
        }
    };

    //
    // assertion_failed
    //  * user defined
    //
    void
    assertion_failed(sprout::assertion_info const&);
}   // namespace sprout_adl

namespace sprout {
    namespace detail {
        inline bool
        assertion_failed(bool cond, char const* formatted, char const* expr, char const* function, char const* file, long line) {
            return cond ? true
                : ((void)sprout::assertion_failed(sprout::assertion_info(expr, function, file, line)), false)
                ;
        }
        inline SPROUT_CONSTEXPR bool
        assertion_check(bool cond, char const* formatted, char const* expr, char const* function, char const* file, long line) {
            return cond ? true
                : sprout::detail::assertion_failed(cond, formatted, expr, function, file, line)
                ;
        }
    }   // namespace detail
}   // namespace sprout

#   define SPROUT_ASSERT(expr) ( \
        (void)sprout::detail::assertion_check( \
            (expr), SPROUT_ASSERTION_FAILED_FORMAT(expr, __FILE__, __LINE__), \
            #expr, "(unknown)"/*SPROUT_CURRENT_FUNCTION*/, __FILE__, __LINE__ \
            ) \
        )

#else

namespace sprout {
    namespace detail {
        inline bool
        assertion_failed(char const* formatted) {
            return (std::cerr << formatted << std::endl), std::abort(), false;
        }
        inline SPROUT_CONSTEXPR bool
        assertion_check(bool cond, char const* formatted) {
            return cond ? true
                : sprout::detail::assertion_failed(formatted)
                ;
        }
    }   // namespace detail
}   // namespace sprout

#   define SPROUT_ASSERT(expr) \
        ((void)sprout::detail::assertion_check((expr), SPROUT_ASSERTION_FAILED_FORMAT(expr, __FILE__, __LINE__)))

#endif

//
// SPROUT_ASSERT_MSG
//

#if defined(SPROUT_DISABLE_ASSERTS) || defined(NDEBUG)

#   define SPROUT_ASSERT_MSG(expr, msg) \
        ((void)0)

#elif defined(SPROUT_ENABLE_ASSERT_HANDLER)

namespace sprout {
    //
    // assertion_info_msg
    //
    class assertion_info_msg {
    private:
        char const* expr_;
        char const* msg_;
        char const* function_;
        char const* file_;
        long line_;
    public:
        SPROUT_CONSTEXPR assertion_info_msg(char const* expr, char const* msg, char const* function, char const* file, long line)
            : expr_(expr), msg_(msg), function_(function), file_(file), line_(line)
        {}
        SPROUT_CONSTEXPR char const* expr() const SPROUT_NOEXCEPT {
            return expr_;
        }
        SPROUT_CONSTEXPR char const* msg() const SPROUT_NOEXCEPT {
            return msg_;
        }
        SPROUT_CONSTEXPR char const* function() const SPROUT_NOEXCEPT {
            return function_;
        }
        SPROUT_CONSTEXPR char const* file() const SPROUT_NOEXCEPT {
            return file_;
        }
        SPROUT_CONSTEXPR long line() const SPROUT_NOEXCEPT {
            return line_;
        }
    };

    //
    // assertion_failed_msg
    //  * user defined
    //
    void
    assertion_failed_msg(sprout::assertion_info_msg const&);
}   // namespace sprout_adl

namespace sprout {
    namespace detail {
        inline bool
        assertion_failed_msg(bool cond, char const* formatted, char const* expr, char const* msg, char const* function, char const* file, long line) {
            return cond ? true
                : ((void)sprout::assertion_failed_msg(sprout::assertion_info_msg(expr, msg, function, file, line)), false)
                ;
        }
        inline SPROUT_CONSTEXPR bool
        assertion_check_msg(bool cond, char const* formatted, char const* expr, char const* msg, char const* function, char const* file, long line) {
            return cond ? true
                : sprout::detail::assertion_failed_msg(cond, formatted, expr, msg, function, file, line)
                ;
        }
    }   // namespace detail
}   // namespace sprout

#   define SPROUT_ASSERT_MSG(expr, msg) ( \
        (void)sprout::detail::assertion_check_msg( \
            (expr), SPROUT_ASSERTION_FAILED_FORMAT(expr, __FILE__, __LINE__), \
            #expr, msg, "(unknown)"/*SPROUT_CURRENT_FUNCTION*/, __FILE__, __LINE__ \
            ) \
        )

#else

namespace sprout {
    namespace detail {
        inline bool
        assertion_failed_msg(char const* formatted, char const* msg) {
            return (std::cerr << formatted << ": " << msg << std::endl), std::abort(), false;
        }
        inline SPROUT_CONSTEXPR bool
        assertion_check_msg(bool cond, char const* formatted, char const* msg) {
            return cond ? true
                : sprout::detail::assertion_failed_msg(formatted, msg)
                ;
        }
    }   // namespace detail
}   // namespace sprout

#   define SPROUT_ASSERT_MSG(expr, msg) \
        ((void)sprout::detail::assertion_check_msg((expr), SPROUT_ASSERTION_FAILED_FORMAT(expr, __FILE__, __LINE__), msg))

#endif

//
// SPROUT_VERIFY
//

#if defined(SPROUT_DISABLE_ASSERTS) || (!defined(SPROUT_ENABLE_ASSERT_HANDLER) && defined(NDEBUG))

namespace sprout {
    namespace detail {
        inline SPROUT_CONSTEXPR bool
        verification_disabled(bool) SPROUT_NOEXCEPT {
            return true;
        }
    }   // namespace detail
}   // namespace sprout

#   define SPROUT_VERIFY(expr) \
        ((void)(sprout::detail::verification_disabled((expr))))

#else

#   define SPROUT_VERIFY(expr) \
        SPROUT_ASSERT(expr)

#endif

#endif  // #ifndef SPROUT_ASEERT_HPP


プリプロセッサによる実装分岐でやたらと長くなっているが、例えば SPROUT_ASSERT のデフォルトの実装は以下の部分である。

    • SPROUT_ASSERT
namespace sprout {
    namespace detail {
        inline bool
        assertion_failed(char const* formatted) {
            return (std::cerr << formatted << std::endl), std::abort(), false;
        }
        inline SPROUT_CONSTEXPR bool
        assertion_check(bool cond, char const* formatted) {
            return cond ? true
                : sprout::detail::assertion_failed(formatted)
                ;
        }
    }   // namespace detail
}   // namespace sprout

#   define SPROUT_ASSERT(expr) \
        ((void)sprout::detail::assertion_check((expr), SPROUT_ASSERTION_FAILED_FORMAT(expr, __FILE__, __LINE__)))

(void) にキャストしているのは、オーバーロードされた operator,() の呼出を避けるためである。
assertion_failed が constexpr 指定されていないことに注意されたし。
非 constexpr 関数の呼出は非定数式であるから、コンパイル時に呼出が評価されると、コンパイルエラーになる。
(なお、ショートサーキット評価が行われうる文脈では、たとえ式中に非定数式を含んでいても、実際に評価されるまではコンパイルエラーにならない)


もちろん、実行時にはそのまま呼出が行われる。
assertion_failed は、標準 assert の失敗時と同じく、文字列化式、ファイル名、行数を含んだエラー文字列を標準エラーに出力し、abort を呼び出してプログラムを終了する。
また、GCC と Clang のコンパイラは、非 constexpr 関数の呼出でコンパイルエラーになったとき、その原因となった呼出をトレースし、引数も含めて表示する。
だから、コンパイル時にアサーション失敗した場合にも、実行時と同じエラー文字列を表示することができる。

アサーションの使用

以下は、ゼロ除算になる場合にエラーとなるアサーションである。

#include <sprout/assert.hpp>

template<typename T>
constexpr T
div(T num, T denom) {
    return SPROUT_ASSERT(denom != 0),
        (num / denom)
        ;
}

int main() {
    constexpr auto x = div(3.14, 0.0);
    (void)x;
}
    • GCC 4.7.2 の出力
In file included from a.cpp:1:0:
/home/boleros/git/sprout/sprout/assert.hpp: 関数 ‘int main()’ 内:
a.cpp:12:34:   in constexpr expansion of ‘div<double>(3.1400000000000001e+0, 0.0)’
a.cpp:7:15:   in constexpr expansion of ‘sprout::detail::assertion_check((denom != 0.0), ((const char*)"***** Internal Program Error - assertion (denom != 0) failed: a.cpp(6)"))’
/home/boleros/git/sprout/sprout/assert.hpp:109:49: エラー: call to non-constexpr function ‘bool sprout::detail::assertion_failed(const char*)’
    • Clang 3.2 の出力
a.cpp:12:17: error: constexpr variable 'x' must be initialized by a constant expression
        constexpr auto x = div(3.14, 0.0);
                       ^   ~~~~~~~~~~~~~~
/home/boleros/git/sprout/sprout/assert.hpp:109:7: note: non-constexpr function 'assertion_failed' cannot be used in a constant expression
                                : sprout::detail::assertion_failed(formatted)
                                  ^
a.cpp:6:9: note: in call to 'assertion_check(false, &"***** Internal Program Error - assertion (denom != 0) failed: a.cpp(6)"[0])'
        return SPROUT_ASSERT(denom != 0),
               ^
/home/boleros/git/sprout/sprout/assert.hpp:116:10: note: expanded from macro 'SPROUT_ASSERT'
                ((void)sprout::detail::assertion_check((expr), SPROUT_ASSERTION_FAILED_FORMAT(expr, __FILE__, __LINE__)))
                       ^
a.cpp:12:21: note: in call to 'div(3.140000e+00, 0.000000e+00)'
        constexpr auto x = div(3.14, 0.0);
                           ^
/home/boleros/git/sprout/sprout/assert.hpp:103:3: note: declared here
                assertion_failed(char const* formatted) {
                ^
1 error generated.

GCC と Clang いずれの場合も、"***** Internal Program Error - assertion (denom != 0) failed: a.cpp(6)" というエラー情報を含んだ文字列が表示されている。


まったく同じコードで、実行時のアサーションも機能する。

#include <sprout/assert.hpp>

template<typename T>
constexpr T
div(T num, T denom) {
    return SPROUT_ASSERT(denom != 0),
        (num / denom)
        ;
}

int main() {
    auto x = div(3.14, 0.0);
    (void)x;
}
    • 実行結果
***** Internal Program Error - assertion (denom != 0) failed: a.cpp(6)
アボートしました

コンパイル時と同じ文字列が出力されている。


当然、標準 assert とまったく同じように、非 constexpr 関数内でも使うことができる。

#include <sprout/assert.hpp>

template<typename T>
constexpr T
div(T num, T denom) {
    return SPROUT_ASSERT(denom != 0),
        (num / denom)
        ;
}

int main() {
    auto x = div(0.0, 1.0);
    SPROUT_ASSERT(x != 0);
}
    • 実行結果
***** Internal Program Error - assertion (x != 0) failed: a.cpp(13)
アボートしました

メッセージ付のアサーションの使用

バリエーションとして、任意のメッセージを付加したアサーションを使用することができる。

#include <sprout/assert.hpp>

template<typename T>
constexpr T
div(T num, T denom) {
    return SPROUT_ASSERT_MSG(denom != 0, "divide by zero"),
        (num / denom)
        ;
}

int main() {
    constexpr auto x = div(3.14, 0.0);
    (void)x;
}
    • GCC 4.7.2 の出力
In file included from a.cpp:1:0:
/home/boleros/git/sprout/sprout/assert.hpp: 関数 ‘int main()’ 内:
a.cpp:12:34:   in constexpr expansion of ‘div<double>(3.1400000000000001e+0, 0.0)’
a.cpp:7:15:   in constexpr expansion of ‘sprout::detail::assertion_check_msg((denom != 0.0), ((const char*)"***** Internal Program Error - assertion (denom != 0) failed: a.cpp(6)"), ((const char*)"divide by zero"))’
/home/boleros/git/sprout/sprout/assert.hpp:206:58: エラー: call to non-constexpr function ‘bool sprout::detail::assertion_failed_msg(const char*, const char*)’

"divide by zero" というメッセージも同時に表示されている。

#include <sprout/assert.hpp>

template<typename T>
constexpr T
div(T num, T denom) {
    return SPROUT_ASSERT_MSG(denom != 0, "divide by zero"),
        (num / denom)
        ;
}

int main() {
    auto x = div(3.14, 0.0);
    (void)x;
}
    • 実行結果
***** Internal Program Error - assertion (denom != 0) failed: a.cpp(6): divide by zero
アボートしました

実行時も、やはりメッセージを末尾に付加して出力される。

アサーションの無効化

標準 assert と同じく、マクロ NDEBUG が定義されているとき、アサーションは無効化される。
また、SPROUT_DISABLE_ASSERTS を定義した場合も、アサーションは無効化される。

#define SPROUT_DISABLE_ASSERTS
#include <sprout/assert.hpp>

template<typename T>
constexpr T
div(T num, T denom) {
    return SPROUT_ASSERT(denom != 0),
        (num / denom)
        ;
}

int main() {
    constexpr auto x = div(3.14, 0.0);
    (void)x;
}
    • GCC 4.7.2 の出力
a.cpp: 関数 ‘int main()’ 内:
a.cpp:13:34:   in constexpr expansion of ‘div<double>(3.1400000000000001e+0, 0.0)’
a.cpp:13:34: エラー: ‘(3.1400000000000001e+0 / 0.0)’ is not a constant expression

アサーションが無効化されているため、ゼロ除算が発生している。

ユーザ定義ハンドラの使用

SPROUT_ENABLE_ASSERT_HANDLER を定義した場合、ユーザコードで定義した assertion_failed(または assertion_failed_msg)が実行時に呼ばれるようになる。
これによって、例えばアサーション失敗時に例外を投げるようにするなど、カスタマイズすることができる。

    • ユーザ定義ハンドラ(実行時)
#define SPROUT_ENABLE_ASSERT_HANDLER
#include <sprout/assert.hpp>
#include <stdexcept>
#include <sstream>
#include <iostream>

namespace sprout {
    void
    assertion_failed(sprout::assertion_info const& info) {
        std::ostringstream os;
        os << "***** Internal Program Error - assertion (" << info.expr() << ") failed in " << info.function() << ": " << info.file() << "(" << info.line() << ")";
        throw std::runtime_error(os.str());
    }
    void
    assertion_failed_msg(sprout::assertion_info_msg const& info) {
        std::ostringstream os;
        os << "***** Internal Program Error - assertion (" << info.expr() << ") failed in " << info.function() << ": " << info.file() << "(" << info.line() << "): " << info.msg();
        throw std::runtime_error(os.str());
    }
}

template<typename T>
constexpr T
div(T num, T denom) {
    return SPROUT_ASSERT(denom != 0),
        (num / denom)
        ;
}

int main() {
    try {
        auto x = div(3.14, 0.0);
        (void)x;
    } catch (std::exception& e) {
        std::cout
            << "exception handled:" << std::endl
            << "what: " << e.what() << std::endl
            ;
    }
}
    • 実行結果
exception handled:
what: ***** Internal Program Error - assertion (denom != 0) failed in (unknown): a.cpp(25)


なお、コンパイル時には、変わらず単にコンパイルエラーになる。

#define SPROUT_ENABLE_ASSERT_HANDLER
#include <sprout/assert.hpp>
#include <stdexcept>
#include <sstream>
#include <iostream>

namespace sprout {
    void
    assertion_failed(sprout::assertion_info const& info) {
        std::ostringstream os;
        os << "***** Internal Program Error - assertion (" << info.expr() << ") failed in " << info.function() << ": " << info.file() << "(" << info.line() << ")";
        throw std::runtime_error(os.str());
    }
    void
    assertion_failed_msg(sprout::assertion_info_msg const& info) {
        std::ostringstream os;
        os << "***** Internal Program Error - assertion (" << info.expr() << ") failed in " << info.function() << ": " << info.file() << "(" << info.line() << "): " << info.msg();
        throw std::runtime_error(os.str());
    }
}

template<typename T>
constexpr T
div(T num, T denom) {
    return SPROUT_ASSERT(denom != 0),
        (num / denom)
        ;
}

int main() {
    try {
        constexpr auto x = div(3.14, 0.0);
        (void)x;
    } catch (std::exception& e) {
        std::cout
            << "exception handled:" << std::endl
            << "what: " << e.what() << std::endl
            ;
    }
}
    • GCC 4.7.2 の出力
In file included from a.cpp:2:0:
/home/boleros/git/sprout/sprout/assert.hpp: 関数 ‘int main()’ 内:
a.cpp:32:35:   in constexpr expansion of ‘div<double>(3.1400000000000001e+0, 0.0)’
a.cpp:26:15:   in constexpr expansion of ‘sprout::detail::assertion_check((denom != 0.0), ((const char*)"***** Internal Program Error - assertion (denom != 0) failed: a.cpp(25)"), ((const char*)"denom != 0"), ((const char*)"(unknown)"), ((const char*)"a.cpp"), 25l)’
/home/boleros/git/sprout/sprout/assert.hpp:85:83: エラー: call to non-constexpr function ‘bool sprout::detail::assertion_failed(bool, const char*, const char*, const char*, const char*, long int)’

結論

このように、Sprout.Assert は、標準 assert や Boost.Assert のほぼ上位互換として用いることができる。


また Sprout には、この他に tuple や optional や variant など、返値で Maybe や Either を表現することのできる道具もある。
constexpr でも非 constexpr 関数と同様に、アサーションや例外や返値によって、好みの方法でエラー通知を行うことができることがわかる。