diff --git a/README.md b/README.md index 2566ac8..f616ceb 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 @@ -196,7 +196,11 @@ 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 -the contract. +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 diff --git a/include/cjdb/contracts.hpp b/include/cjdb/contracts.hpp index b13c846..7a2cf45 100644 --- a/include/cjdb/contracts.hpp +++ b/include/cjdb/contracts.hpp @@ -1,13 +1,22 @@ + // Copyright (c) Christopher Di Bella. // SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception // #ifndef CJDB_CONTRACTS_HPP #define CJDB_CONTRACTS_HPP -#include +#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 +33,21 @@ #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 @@ -32,14 +55,28 @@ namespace cjdb::contracts_detail { #endif // NDEBUG struct contract_impl_fn { + template constexpr void operator()(bool const result, - std::string_view const message, - std::string_view const function) const noexcept + char const(&message)[N1], // NOLINT(modernize-avoid-c-arrays) + char const(&function)[N2]) const noexcept // NOLINT(modernize-avoid-c-arrays) { if (not result) { if (not std::is_constant_evaluated()) { - if constexpr (is_debug) { - std::fprintf(stderr, "%s in `%s`\n", message.data(), function.data()); + if constexpr (is_debug) { // NOLINT + #ifdef _WIN32 + constexpr auto& suffix = "`\r\n"; + #else + constexpr auto& suffix = "`\n"; + #endif // _WIN32 + constexpr auto message_size = N1 - 1; + constexpr auto function_size = N2 - 1; + // NOLINTNEXTLINE(modernize-avoid-c-arrays) + char full_message[message_size + function_size + sizeof suffix]{}; + auto p = full_message; + std::memcpy(p, message, message_size); + std::memcpy(p += message_size, function, function_size); + std::memcpy(p += function_size, suffix, sizeof suffix - 1); + ::cjdb::print_error(full_message); } } #ifdef _MSC_VER @@ -65,11 +102,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)