diff --git a/CMakeLists.txt b/CMakeLists.txt index b926e28..6132797 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -7,7 +7,7 @@ if(ESP_PLATFORM) endif() project(frequency - VERSION 1.2.0 + VERSION 1.2.1 DESCRIPTION "Type-safe frequency handling library modeled after std::chrono" HOMEPAGE_URL "https://github.com/cleishm/frequency-cpp" LANGUAGES CXX diff --git a/docs/Doxyfile b/docs/Doxyfile index cf11061..acf6a1f 100644 --- a/docs/Doxyfile +++ b/docs/Doxyfile @@ -6,7 +6,7 @@ DOXYFILE_ENCODING = UTF-8 PROJECT_NAME = "frequency" -PROJECT_NUMBER = "1.2.0" +PROJECT_NUMBER = "1.2.1" PROJECT_BRIEF = "Type-safe frequency handling library modeled after std::chrono" PROJECT_LOGO = OUTPUT_DIRECTORY = . diff --git a/idf_component.yml b/idf_component.yml index 389133f..3d24100 100644 --- a/idf_component.yml +++ b/idf_component.yml @@ -1,4 +1,4 @@ -version: "1.2.0" +version: "1.2.1" description: "Type-safe frequency handling library modeled after std::chrono" url: "https://github.com/cleishm/frequency-cpp" repository: "https://github.com/cleishm/frequency-cpp.git" diff --git a/include/frequency/frequency.hpp b/include/frequency/frequency.hpp index 1b4362c..3a39141 100644 --- a/include/frequency/frequency.hpp +++ b/include/frequency/frequency.hpp @@ -9,6 +9,7 @@ #include #include #include +#include #include #include #include @@ -1156,6 +1157,25 @@ constexpr OutputIt _format_append(OutputIt out, const char* s) { } return out; } + +#if CONFIG_FREQUENCY_STD_FORMAT +// Reports an unusable format spec from a formatter's parse(). +// +// std::format constant-evaluates parse() to check the format string, so +// throwing there makes a bad spec a compile error rather than a runtime fault. +// Where exceptions are unavailable, calling a non-constexpr function fails that +// same constant evaluation and so reports the error at compile time too; a +// runtime parse (std::vformat with a runtime format string) has no way to +// report it and terminates. +[[noreturn]] inline void _format_error(const char* what) { +#if defined(__cpp_exceptions) && __cpp_exceptions + throw std::format_error(what); +#else + (void)what; + std::abort(); +#endif +} +#endif /** @endcond */ /** @@ -1201,8 +1221,8 @@ struct formatter> { constexpr auto parse(format_parse_context& ctx) { auto it = ctx.begin(); if (it == ctx.end() || *it == '}') { - if (_prefix == nullptr) { - throw format_error("frequency: precision has no SI prefix; use an explicit format spec"); + if constexpr (_prefix == nullptr) { + freq::_format_error("frequency: precision has no SI prefix; use an explicit format spec"); } return it; } diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index bed04e2..3e52518 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -21,3 +21,14 @@ target_compile_options(frequency_tests PRIVATE -Wall) include(CTest) include(Catch) catch_discover_tests(frequency_tests) + +# The formatter must also build with exceptions disabled, as on embedded +# targets. Catch2 requires exceptions, so this is a standalone target. MSVC +# spells the option differently and still declares __cpp_exceptions, so the +# check is limited to compilers taking -fno-exceptions. +if(NOT MSVC) + add_executable(frequency_no_exceptions_test no_exceptions_test.cpp) + target_link_libraries(frequency_no_exceptions_test PRIVATE frequency::frequency) + target_compile_options(frequency_no_exceptions_test PRIVATE -fno-exceptions) + add_test(NAME no_exceptions COMMAND frequency_no_exceptions_test) +endif() diff --git a/tests/no_exceptions_test.cpp b/tests/no_exceptions_test.cpp new file mode 100644 index 0000000..4a9ab10 --- /dev/null +++ b/tests/no_exceptions_test.cpp @@ -0,0 +1,66 @@ +// Regression test: the std::formatter specialization must be usable when +// compiled with exceptions disabled (-fno-exceptions), as on embedded targets +// such as ESP-IDF with CONFIG_COMPILER_CXX_EXCEPTIONS unset. +// +// This is a separate target from frequency_tests because Catch2 requires +// exceptions. It deliberately avoids any test framework so the whole +// translation unit builds without them. + +#include +#include +#include + +#ifdef __cpp_exceptions +#error "this test must be compiled with exceptions disabled" +#endif + +#if !CONFIG_FREQUENCY_STD_FORMAT + +// Without std::format the formatter is not compiled and there is nothing to +// test; older standard libraries (e.g. g++-12's) lack entirely. +int main() { + std::printf("std::format unavailable; nothing to test\n"); + return 0; +} + +#else + +using namespace freq; + +namespace { + +int failures = 0; + +void check(const std::string& actual, const std::string& expected, const char* what) { + if (actual != expected) { + std::printf("FAIL: %s: expected \"%s\", got \"%s\"\n", what, expected.c_str(), actual.c_str()); + ++failures; + } +} + +} // namespace + +int main() { + // Formatting with no spec: instantiating parse() must not pull in a throw. + check(std::format("{}", hertz(50)), "50Hz", "{} hertz"); + check(std::format("{}", millihertz(1500)), "1500mHz", "{} millihertz"); + check(std::format("{}", kilohertz(433)), "433kHz", "{} kilohertz"); + check(std::format("{}", megahertz(868)), "868MHz", "{} megahertz"); + + // Formatting with an explicit spec. + check(std::format("{:.1f}", millihertz(1500)), "1.5Hz", "{:.1f} millihertz"); + check(std::format("{:.1f}", kilohertz(433)), "433000.0Hz", "{:.1f} kilohertz"); + + // A precision with no SI prefix is still usable with an explicit spec. + using decihertz = frequency; + check(std::format("{:.1f}", decihertz(225)), "22.5Hz", "{:.1f} decihertz"); + + if (failures != 0) { + std::printf("%d check(s) failed\n", failures); + return 1; + } + std::printf("all checks passed\n"); + return 0; +} + +#endif // CONFIG_FREQUENCY_STD_FORMAT diff --git a/vcpkg-port/vcpkg.json b/vcpkg-port/vcpkg.json index 319af6e..d17f9dd 100644 --- a/vcpkg-port/vcpkg.json +++ b/vcpkg-port/vcpkg.json @@ -1,6 +1,6 @@ { "name": "cleishm-frequency-cpp", - "version": "1.2.0", + "version": "1.2.1", "description": "Type-safe frequency handling library modeled after std::chrono", "homepage": "https://github.com/cleishm/frequency-cpp", "license": "MIT",