Commit b19c8721 authored by Dominik Charousset's avatar Dominik Charousset

Add format API scaffolding

The new `detail::format` and `detail::format_to` functions enable
format-based outputs in the new unit testing framework and gives us the
option to re-write the logging API without relying on the `std::ostream`
interfaces. These functions provide a thin wrapper around `std::format`.
In case the standard version is not available, we fall back to a minimal
implementation that delegates formatting of floats and integers to
`snprintf`.
parent 9498fc8a
......@@ -6,6 +6,7 @@ project(CAF CXX)
include(CMakeDependentOption)
include(CMakePackageConfigHelpers)
include(CheckCXXSourceCompiles)
include(CheckCXXSourceRuns)
include(GNUInstallDirs)
include(GenerateExportHeader)
......@@ -158,6 +159,26 @@ if(NOT CMAKE_CROSSCOMPILING)
endif()
endif()
# -- check whether we can use std::format --------------------------------------
if(NOT DEFINED CAF_USE_STD_FORMAT)
set(CAF_USE_STD_FORMAT OFF CACHE BOOL "Enable std::format support" FORCE)
if(NOT CMAKE_CROSSCOMPILING)
set(snippet "#include <format>
#include <iostream>
int main() { std::cout << std::format(\"{}\", \"ok\"); }")
check_cxx_source_runs("${snippet}" snippet_output)
if("${snippet_output}" STREQUAL "ok")
message(STATUS "Enable std::format support")
set(CAF_USE_STD_FORMAT ON CACHE BOOL "Enable std::format support" FORCE)
else()
message(STATUS "Disable std::format support: NOT available")
endif()
endif()
else()
set(CAF_USE_STD_FORMAT OFF CACHE BOOL "Enable std::format support" FORCE)
endif()
# -- export internal target (may be useful for re-using compiler flags) --------
set_target_properties(caf_internal PROPERTIES EXPORT_NAME internal)
......
......@@ -11,3 +11,5 @@
#cmakedefine CAF_ENABLE_EXCEPTIONS
#cmakedefine CAF_ENABLE_ACTOR_PROFILER
#cmakedefine CAF_USE_STD_FORMAT
......@@ -255,6 +255,7 @@ caf_add_component(
detail.base64
detail.bounds_checker
detail.config_consumer
detail.format
detail.group_tunnel
detail.ieee_754
detail.json
......@@ -385,6 +386,10 @@ caf_add_component(
uri
uuid)
if(NOT CAF_USE_STD_FORMAT)
target_sources(libcaf_core_obj PRIVATE src/detail/format.cpp)
endif()
if(CAF_ENABLE_TESTING AND CAF_ENABLE_EXCEPTIONS)
caf_add_test_suites(caf-core-test custom_exception_handler)
endif()
// This file is part of CAF, the C++ Actor Framework. See the file LICENSE in
// the main distribution directory for license terms and copyright or visit
// https://github.com/actor-framework/actor-framework/blob/master/LICENSE.
#pragma once
// Thin wrapper around std::format. If that is not available, we provide a
// minimal implementation that is "good enough" for our purposes.
//
// TODO: Currently, this only wraps the format_to and format functions as-is.
// The wrappers should also add support for types that provide an
// `inspect` overload.
#include "caf/detail/build_config.hpp"
#include <string>
#ifdef CAF_USE_STD_FORMAT
# include <format>
namespace caf::detail {
template <class OutputIt, class... Args>
auto format_to(OutputIt out, std::format_string<Args...> fstr, Args&&... args) {
return std::format_to(out, fstr, std::forward<Args>(args)...);
}
template <class... Args>
std::string format(std::format_string<Args...> fstr, Args&&... args) {
return std::format(fstr, std::forward<Args>(args)...);
}
} // namespace caf::detail
#else // here comes the poor man's version
# include "caf/detail/core_export.hpp"
# include "caf/span.hpp"
# include <array>
# include <cstdint>
# include <iterator>
# include <memory>
# include <string_view>
# include <variant>
namespace caf::detail {
using format_arg = std::variant<bool, char, int64_t, uint64_t, double,
const char*, std::string_view, const void*>;
template <class T>
format_arg make_format_arg(const T& arg) {
if constexpr (std::is_same_v<T, bool>) {
return format_arg{arg};
} else if constexpr (std::is_same_v<T, char>) {
return format_arg{arg};
} else if constexpr (std::is_integral_v<T>) {
if constexpr (std::is_signed_v<T>) {
return format_arg{static_cast<int64_t>(arg)};
} else {
return format_arg{static_cast<uint64_t>(arg)};
}
} else if constexpr (std::is_floating_point_v<T>) {
return format_arg{static_cast<double>(arg)};
} else if constexpr (std::is_same_v<T, const char*>) {
return format_arg{arg};
} else if constexpr (std::is_same_v<T, std::string>) {
return format_arg{std::string_view{arg}};
} else if constexpr (std::is_same_v<T, std::string_view>) {
return format_arg{arg};
} else {
static_assert(std::is_pointer_v<T>, "unsupported argument type");
return format_arg{static_cast<const void*>(arg)};
}
}
template <size_t N>
format_arg make_format_arg(const char (&arg)[N]) {
return format_arg{std::string_view{arg, N - 1}};
}
// Interface traversing the formatting output chunk by chunk.
class CAF_CORE_EXPORT compiled_format_string {
public:
virtual ~compiled_format_string();
// Checks whether we reached the end of the format string.
virtual bool at_end() const noexcept = 0;
// Returns the next chunk of the formatted output.
virtual std::string_view next() = 0;
};
CAF_CORE_EXPORT
std::unique_ptr<compiled_format_string>
compile_format_string(std::string_view fstr, span<format_arg> args);
template <class OutputIt, class... Args>
auto format_to(OutputIt out, std::string_view fstr, Args&&... raw_args) {
std::array<format_arg, sizeof...(Args)> args{make_format_arg(raw_args)...};
auto compiled = compile_format_string(fstr, make_span(args));
while (!compiled->at_end()) {
auto chunk = compiled->next();
out = std::copy(chunk.begin(), chunk.end(), out);
}
return out;
}
template <class... Args>
std::string format(std::string_view fstr, Args&&... args) {
std::string result;
result.reserve(fstr.size());
format_to(std::back_inserter(result), fstr, std::forward<Args>(args)...);
return result;
}
} // namespace caf::detail
#endif
This diff is collapsed.
// This file is part of CAF, the C++ Actor Framework. See the file LICENSE in
// the main distribution directory for license terms and copyright or visit
// https://github.com/actor-framework/actor-framework/blob/master/LICENSE.
#define CAF_SUITE detail.format
#include "caf/detail/format.hpp"
#include "core-test.hpp"
#if !defined(CAF_USE_STD_FORMAT) && !defined(CAF_USE_SYSTEM_LIBFMT)
# define CAF_MINIMAL_FORMATTING
#endif
using namespace std::literals;
using caf::detail::format;
using caf::detail::format_to;
TEST_CASE("format strings without placeholders copies verbatim") {
CHECK_EQ(format("hello world"), "hello world");
CHECK_EQ(format("foo {{bar}}"), "foo {bar}");
CHECK_EQ(format("foo {{bar}} baz"), "foo {bar} baz");
}
TEST_CASE("format strings without indexes iterate over their arguments") {
CHECK_EQ(format("foo: {}{}", true, '!'), "foo: true!");
CHECK_EQ(format("bar: {}{}", false, '?'), "bar: false?");
CHECK_EQ(format("{} {} {} {} {}", 1, 2u, 2.5f, 4.5, "5"s), "1 2 2.5 4.5 5");
}
TEST_CASE("format strings with indexes uses the specified arguments") {
CHECK_EQ(format("{1} {2} {0}", 3, 1, 2), "1 2 3");
CHECK_EQ(format("{1} {0} {1}", 1, 2), "2 1 2");
}
TEST_CASE("format strings can specify rendering of floating point numbers") {
CHECK_EQ(format("{}", 2.5), "2.5");
CHECK_EQ(format("{:.3f}", 2.5), "2.500");
CHECK_EQ(format("{:.3F}", 2.5), "2.500");
CHECK_EQ(format("{:g}", 2.5), "2.5");
CHECK_EQ(format("{:G}", 2.5), "2.5");
CHECK_EQ(format("{:.0e}", 10.0), "1e+01");
CHECK_EQ(format("{:.0E}", 10.0), "1E+01");
}
TEST_CASE("format strings can specify rendering of integers") {
CHECK_EQ(format("{}", 42), "42");
CHECK_EQ(format("{:d}", 42), "42");
CHECK_EQ(format("{:c}", 42), "*");
CHECK_EQ(format("{:o}", 42), "52");
CHECK_EQ(format("{:#o}", 42), "052");
CHECK_EQ(format("{:x}", 42), "2a");
CHECK_EQ(format("{:X}", 42), "2A");
CHECK_EQ(format("{:#x}", 42), "0x2a");
CHECK_EQ(format("{:#X}", 42), "0X2A");
CHECK_EQ(format("{}", 42u), "42");
CHECK_EQ(format("{:d}", 42u), "42");
CHECK_EQ(format("{:c}", 42u), "*");
CHECK_EQ(format("{:o}", 42u), "52");
CHECK_EQ(format("{:#o}", 42u), "052");
CHECK_EQ(format("{:x}", 42u), "2a");
CHECK_EQ(format("{:X}", 42u), "2A");
CHECK_EQ(format("{:#x}", 42u), "0x2a");
CHECK_EQ(format("{:#X}", 42u), "0X2A");
CHECK_EQ(format("'{:+}' '{:-}' '{: }'", 1, 1, 1), "'+1' '1' ' 1'");
CHECK_EQ(format("'{:+}' '{:-}' '{: }'", -1, -1, -1), "'-1' '-1' '-1'");
}
TEST_CASE("format strings may specify the width of the output") {
CHECK_EQ(format("{0:0{1}}", 1, 2), "01");
CHECK_EQ(format("{1:02} {0:02}", 1, 2), "02 01");
CHECK_EQ(format("{:!<3}?{:!>3}", 0, 0), "0!!?!!0");
CHECK_EQ(format("{:!^3}?{:!^3}", 'A', 'A'), "!A!?!A!");
CHECK_EQ(format("{0:!^{1}}", 'A', 5), "!!A!!");
CHECK_EQ(format("{:<3}?{:>3}", 0, 0), "0 ? 0");
}
TEST_CASE("format strings accept various string types as values") {
using namespace std::literals;
const auto* cstr = "C-string";
CHECK_EQ(format("{}", cstr), "C-string");
CHECK_EQ(format("{}", "string literal"), "string literal");
CHECK_EQ(format("{}", "std::string"s), "std::string");
CHECK_EQ(format("{}", "std::string_view"sv), "std::string_view");
}
TEST_CASE("format_to can incrementally build a string") {
std::string str;
format_to(std::back_inserter(str), "foo");
CHECK_EQ(str, "foo");
format_to(std::back_inserter(str), "bar");
CHECK_EQ(str, "foobar");
format_to(std::back_inserter(str), "baz");
CHECK_EQ(str, "foobarbaz");
}
#if defined(CAF_ENABLE_EXCEPTIONS) && defined(CAF_MINIMAL_FORMATTING)
// Note: the standard version as well as libfmt (should) raise a compile-time
// error for these test cases. Only our minimal implementation throws.
TEST_CASE("ill-formatted formatting strings throw") {
CHECK_THROWS_AS(format("foo {"), std::logic_error);
CHECK_THROWS_AS(format("foo } bar"), std::logic_error);
CHECK_THROWS_AS(format("{1}", 1), std::logic_error);
}
#endif
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment