コンパイル時/実行時両用アサート
中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 関数内でも使うことができる。
-
- 非 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 関数と同様に、アサーションや例外や返値によって、好みの方法でエラー通知を行うことができることがわかる。