Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Optimize and add ability to configure error printing function #11

Open
wants to merge 14 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 13 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 7 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 `<cassert>` 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
Expand Down Expand Up @@ -196,7 +196,11 @@ generation than when the contract isn't used. [See for yourself][__builtin_unrea
</table>

*Rudimentary testing has identified that neither GCC nor Clang perform optimisations <em>before</em>
the contract.
the contract.*
rayhamel marked this conversation as resolved.
Show resolved Hide resolved

### 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

Expand Down
41 changes: 35 additions & 6 deletions include/cjdb/contracts.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,17 @@
#ifndef CJDB_CONTRACTS_HPP
#define CJDB_CONTRACTS_HPP

#include <cstdio>
#include <string_view>
#include <type_traits>

#ifdef CJDB_USE_IOSTREAM
#include <iostream>
#elif !defined(CJDB_SKIP_STDIO)
#include <cerrno>
#include <cstdio>
#include <system_error>
#endif // CJDB_USE_IOSTREAM

// clang-tidy doesn't yet support this
//
// #ifndef __cpp_lib_is_constant_evaluated
Expand All @@ -24,7 +31,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<std::streamsize>(message.size()));
#elif !defined(CJDB_SKIP_STDIO)
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is the motivation for skipping I/O?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. For use in freestanding implementations that lack an output device and/or the header cstdio
  2. For users who provide their own cjdb::print_error function. While this use case is possible without a configuration option, it's not necessarily desirable to #include <cstdio> with all that implies (e.g. declaring a function called remove in the global namespace).

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
Expand All @@ -34,12 +55,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
Expand All @@ -65,11 +93,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)


Expand Down
11 changes: 11 additions & 0 deletions test/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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)
Expand Down