diff --git a/README.md b/README.md index 2566ac8..807f647 100644 --- a/README.md +++ b/README.md @@ -59,8 +59,8 @@ contracts are always checked at compile-time. If the contract's predicate isn't program won't compile. This is a huge advantage over using `` or the GSL's `Expects` and `Ensures` macros. -When optimisations are diabled and `NDEBUG` is not defined as a macro, the contract will check your -predicate at run-time. If the predicate fails, then a diagnostic will be emit, and the program will +When optimisations are disabled and `NDEBUG` is not defined as a macro, the contract will check your +predicate at run-time. If the predicate fails, then a diagnostic will be emitted, and the program will crash. When optimisations are enabled, and `NDEBUG` remains undefined, the program will emit a diagnostic @@ -195,9 +195,18 @@ generation than when the contract isn't used. [See for yourself][__builtin_unrea -*Rudimentary testing has identified that neither GCC nor Clang perform optimisations before +\*Rudimentary testing has identified that neither GCC nor Clang perform optimisations before the contract. +### Configuring diagnostics + +By default, diagnostic messages are printed to `stderr` by `std::fwrite`. If `CJDB_USE_IOSTREAM` is +defined as a macro, messages are printed with `std::cerr.write` instead. If `CJDB_SKIP_STDIO` is +defined as a macro, there is no dependency on either `cstdio` or `iostream` and printing diagnostic +messages is a no-op. If you would like to customize how diagnostics are printed, you may set the +function pointer `cjdb::print_error` to any function or lambda with the signature +`void(std::string_view)`. + ### Assertions You should use assertions to indicate when a programming error has occurred in the middle of your diff --git a/include/cjdb/contracts.hpp b/include/cjdb/contracts.hpp index b13c846..3b167ba 100644 --- a/include/cjdb/contracts.hpp +++ b/include/cjdb/contracts.hpp @@ -4,10 +4,17 @@ #ifndef CJDB_CONTRACTS_HPP #define CJDB_CONTRACTS_HPP -#include #include #include +#ifdef CJDB_USE_IOSTREAM + #include +#elif !defined(CJDB_SKIP_STDIO) + #include + #include + #include +#endif // CJDB_USE_IOSTREAM + // clang-tidy doesn't yet support this // // #ifndef __cpp_lib_is_constant_evaluated @@ -24,7 +31,22 @@ #define CJDB_PRETTY_FUNCTION __PRETTY_FUNCTION__ #endif // _MSC_VER -namespace cjdb::contracts_detail { +namespace cjdb { + using print_error_fn = void(std::string_view); + inline print_error_fn* print_error = [](std::string_view message) + { + #ifdef CJDB_USE_IOSTREAM + std::cerr.write(message.data(), static_cast(message.size())); + #elif !defined(CJDB_SKIP_STDIO) + if (auto const len = message.size(); + std::fwrite(message.data(), sizeof(char), len, stderr) < len) [[unlikely]] + { + throw std::system_error{errno, std::system_category()}; + } + #endif // CJDB_USE_IOSTREAM + }; + +namespace contracts_detail { #ifdef NDEBUG inline constexpr auto is_debug = false; #else @@ -34,12 +56,19 @@ namespace cjdb::contracts_detail { struct contract_impl_fn { constexpr void operator()(bool const result, std::string_view const message, - std::string_view const function) const noexcept + std::string_view const function) const noexcept(!is_debug) { if (not result) { if (not std::is_constant_evaluated()) { if constexpr (is_debug) { - std::fprintf(stderr, "%s in `%s`\n", message.data(), function.data()); + #ifdef _WIN32 + constexpr auto& suffix = "`\r\n"; + #else + constexpr auto& suffix = "`\n"; + #endif // _WIN32 + ::cjdb::print_error(message); + ::cjdb::print_error(function); + ::cjdb::print_error(suffix); } } #ifdef _MSC_VER @@ -65,11 +94,12 @@ namespace cjdb::contracts_detail { } }; inline constexpr auto matches_bool = matches_bool_fn{}; -} // namespace cjdb::contracts_detail +} // namespace contracts_detail +} // namespace cjdb #define CJDB_CONTRACT_IMPL(CJDB_KIND, ...) \ ::cjdb::contracts_detail::contract_impl(::cjdb::contracts_detail::matches_bool(__VA_ARGS__), \ - __FILE__ ":" CJDB_TO_STRING(__LINE__) ": " CJDB_KIND " `" #__VA_ARGS__ "` failed", \ + __FILE__ ":" CJDB_TO_STRING(__LINE__) ": " CJDB_KIND " `" #__VA_ARGS__ "` failed in `", \ CJDB_PRETTY_FUNCTION) diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 9ae06ef..60fceda 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -3,15 +3,22 @@ # function(build_contract filename) set(target "${filename}") + set(target_ios "${filename}_ios") add_executable("${target}" "${filename}.cpp") + add_executable("${target_ios}" "${filename}.cpp") if(MSVC) target_compile_options("${target}" PRIVATE "/permissive-") + target_compile_options("${target_ios}" PRIVATE "/permissive-" "/DCJDB_USE_IOSTREAM") + else() + target_compile_options("${target_ios}" PRIVATE "-DCJDB_USE_IOSTREAM") endif() target_include_directories("${target}" PRIVATE "${CMAKE_SOURCE_DIR}/include") + target_include_directories("${target_ios}" PRIVATE "${CMAKE_SOURCE_DIR}/include") endfunction() build_contract(pass) add_test(test.pass pass) +add_test(test.pass_ios pass_ios) function(test_contract target expected_output) set(args "${CMAKE_SOURCE_DIR}/test/check-failure.py" @@ -23,6 +30,10 @@ function(test_contract target expected_output) add_test(NAME "test.${target}" COMMAND python3 ${args} WORKING_DIRECTORY "${CMAKE_BINARY_DIR}") + list(TRANSFORM args APPEND "_ios" AT 1) + add_test(NAME "test.${target}_ios" + COMMAND python3 ${args} + WORKING_DIRECTORY "${CMAKE_BINARY_DIR}") endfunction() function(test_quiet_contract target expected_output)