From 3ab77cff58283b75811a1bc5f289f228bc282cac Mon Sep 17 00:00:00 2001 From: Scott Dixon Date: Wed, 21 Oct 2026 15:16:14 -0700 Subject: [PATCH 1/7] Add C++26 saturation arithmetic polyfills (#39) libcyphal and nunavut both need saturating integer operations, and C++26 standardized them (P0543R3, renamed by P4052R0 to saturating_add, saturating_sub, saturating_mul, saturating_div and saturating_cast). CETL now provides those five functions for C++14 and newer, exact to the standard: one operand type that is never promoted, bool and the character types excluded, a y != 0 precondition on division, and min / -1 saturating to max. The compiler's overflow builtins are used where present and a portable implementation that never forms an overflowing expression otherwise; both are constant expressions in C++14 and both are tested. cetl/pf26/cetlpf.hpp selects the implementation by the library feature-test macro rather than __cplusplus, because current compilers still report an interim value for -std=c++26 and because the functions were renamed late: 202603L uses the standard names directly, 202311L forwards to the earlier std::add_sat family under the final names, and anything else uses cetl::pf26. saturating_cast needs sign-correct comparisons, so the C++20 integer comparison functions (cmp_equal, cmp_less, ..., in_range) are added as cetl::pf20 polyfills, and nunavut's "saturated intN" fields need a clamp to an arbitrary range, so std::clamp is added as a cetl::pf17 polyfill. Both are wired into their cetlpf.hpp headers. Tests: exhaustive 8-bit and sampled 16-bit operand pairs, random 32/64-bit operands against a 128-bit oracle, exhaustive saturating_cast from the 8- and 16-bit types into every fixed-width type, an integer-comparison sweep against a sign-aware oracle, debug-assert death tests, compile-failure tests for bool and mixed operand types, and a differential libFuzzer target. A new cpp-26 preset is generated for every toolchain except GCC 7 and added to the amd64 CI matrix so the standard-library paths are exercised. Example 13 and CETL_CPP_STANDARD_23/26 document the addition. Refs #39; the cetl::saturating wrapper type follows in a separate PR. Co-Authored-By: Claude Fable 5.1 --- .github/workflows/cetlvast.yml | 2 +- cetlvast/CMakeLists.txt | 2 +- cetlvast/CMakePresets.json | 649 +++++++++++++++++- cetlvast/CMakePresetsVendorTemplate.json | 9 +- cetlvast/suites/compile/CMakeLists.txt | 3 + .../compile/test_pf20_cmp_less_bool.cpp | 19 + .../compile/test_pf26_saturating_add_bool.cpp | 19 + .../test_pf26_saturating_add_mixed_types.cpp | 19 + cetlvast/suites/docs/examples/CMakeLists.txt | 1 + .../example_13_saturating_arithmetic.cpp | 71 ++ cetlvast/suites/fuzz/CMakeLists.txt | 47 +- .../saturation_arithmetic/int32_min_div_neg1 | Bin 0 -> 17 bytes .../int64_min_times_neg1 | Bin 0 -> 17 bytes .../saturation_arithmetic/int8_min_minus_one | Bin 0 -> 17 bytes .../uint64_max_times_two | Bin 0 -> 17 bytes .../saturation_arithmetic/uint8_max_plus_one | Bin 0 -> 17 bytes .../fuzz/fuzz_saturation_arithmetic.cpp | 139 ++++ cetlvast/suites/unittest/CMakeLists.txt | 3 + .../suites/unittest/test_pf17_algorithm.cpp | 94 +++ .../suites/unittest/test_pf20_utility.cpp | 211 ++++++ .../suites/unittest/test_pf26_numeric.cpp | 495 +++++++++++++ include/cetl/cetl.hpp | 15 + include/cetl/pf17/algorithm.hpp | 47 ++ include/cetl/pf17/cetlpf.hpp | 8 + include/cetl/pf20/cetlpf.hpp | 20 + include/cetl/pf20/utility.hpp | 149 ++++ include/cetl/pf26/cetlpf.hpp | 105 +++ include/cetl/pf26/numeric.hpp | 249 +++++++ 28 files changed, 2345 insertions(+), 31 deletions(-) create mode 100644 cetlvast/suites/compile/test_pf20_cmp_less_bool.cpp create mode 100644 cetlvast/suites/compile/test_pf26_saturating_add_bool.cpp create mode 100644 cetlvast/suites/compile/test_pf26_saturating_add_mixed_types.cpp create mode 100644 cetlvast/suites/docs/examples/example_13_saturating_arithmetic.cpp create mode 100644 cetlvast/suites/fuzz/corpus/saturation_arithmetic/int32_min_div_neg1 create mode 100644 cetlvast/suites/fuzz/corpus/saturation_arithmetic/int64_min_times_neg1 create mode 100644 cetlvast/suites/fuzz/corpus/saturation_arithmetic/int8_min_minus_one create mode 100644 cetlvast/suites/fuzz/corpus/saturation_arithmetic/uint64_max_times_two create mode 100644 cetlvast/suites/fuzz/corpus/saturation_arithmetic/uint8_max_plus_one create mode 100644 cetlvast/suites/fuzz/fuzz_saturation_arithmetic.cpp create mode 100644 cetlvast/suites/unittest/test_pf17_algorithm.cpp create mode 100644 cetlvast/suites/unittest/test_pf20_utility.cpp create mode 100644 cetlvast/suites/unittest/test_pf26_numeric.cpp create mode 100644 include/cetl/pf17/algorithm.hpp create mode 100644 include/cetl/pf20/utility.hpp create mode 100644 include/cetl/pf26/cetlpf.hpp create mode 100644 include/cetl/pf26/numeric.hpp diff --git a/.github/workflows/cetlvast.yml b/.github/workflows/cetlvast.yml index 419f2475..3ae33eae 100644 --- a/.github/workflows/cetlvast.yml +++ b/.github/workflows/cetlvast.yml @@ -88,7 +88,7 @@ jobs: strategy: matrix: toolchain: ["gcc-native-32", "clang-native"] - standard: ["cpp-14", "cpp-17", "cpp-20", "cpp-23"] + standard: ["cpp-14", "cpp-17", "cpp-20", "cpp-23", "cpp-26"] steps: - uses: actions/checkout@v7 with: diff --git a/cetlvast/CMakeLists.txt b/cetlvast/CMakeLists.txt index d63e2930..4107c99d 100644 --- a/cetlvast/CMakeLists.txt +++ b/cetlvast/CMakeLists.txt @@ -78,7 +78,7 @@ message(STATUS # +---------------------------------------------------------------------------+ set(CETLVAST_CPP_STANDARD "20" CACHE STRING "C++ standard to use when compiling.") -set_property(CACHE CETLVAST_CPP_STANDARD PROPERTY STRINGS 14 17 20) +set_property(CACHE CETLVAST_CPP_STANDARD PROPERTY STRINGS 14 17 20 23 26) option(CETLVAST_ENABLE_FUZZING "Build native libFuzzer targets with ASan and UBSan." OFF) if(CETLVAST_ENABLE_FUZZING AND CMAKE_CROSSCOMPILING) diff --git a/cetlvast/CMakePresets.json b/cetlvast/CMakePresets.json index 950d500a..ca07ac03 100644 --- a/cetlvast/CMakePresets.json +++ b/cetlvast/CMakePresets.json @@ -445,6 +445,85 @@ "configure-standard-cpp-23", "configure-update-offline" ] + }, + { + "name": "configure-standard-cpp-26", + "hidden": true, + "cacheVariables": { + "CETLVAST_CPP_STANDARD": "26" + } + }, + { + "name": "configure-gcc-native-cpp-26-online", + "inherits": [ + "configure-common", + "configure-toolchain-gcc-native", + "configure-standard-cpp-26", + "configure-update-online" + ] + }, + { + "name": "configure-gcc-native-cpp-26-offline", + "inherits": [ + "configure-common", + "configure-toolchain-gcc-native", + "configure-standard-cpp-26", + "configure-update-offline" + ] + }, + { + "name": "configure-gcc-native-32-cpp-26-online", + "inherits": [ + "configure-common", + "configure-toolchain-gcc-native-32", + "configure-standard-cpp-26", + "configure-update-online" + ] + }, + { + "name": "configure-gcc-native-32-cpp-26-offline", + "inherits": [ + "configure-common", + "configure-toolchain-gcc-native-32", + "configure-standard-cpp-26", + "configure-update-offline" + ] + }, + { + "name": "configure-clang-native-cpp-26-online", + "inherits": [ + "configure-common", + "configure-toolchain-clang-native", + "configure-standard-cpp-26", + "configure-update-online" + ] + }, + { + "name": "configure-clang-native-cpp-26-offline", + "inherits": [ + "configure-common", + "configure-toolchain-clang-native", + "configure-standard-cpp-26", + "configure-update-offline" + ] + }, + { + "name": "configure-arm-none-eabi-gcc-cpp-26-online", + "inherits": [ + "configure-common", + "configure-toolchain-arm-none-eabi-gcc", + "configure-standard-cpp-26", + "configure-update-online" + ] + }, + { + "name": "configure-arm-none-eabi-gcc-cpp-26-offline", + "inherits": [ + "configure-common", + "configure-toolchain-arm-none-eabi-gcc", + "configure-standard-cpp-26", + "configure-update-offline" + ] } ], "buildPresets": [ @@ -2011,6 +2090,358 @@ "run_examples", "gcovr_html_report_for_examples" ] + }, + { + "name": "build-Release-gcc-native-cpp-26-online", + "configurePreset": "configure-gcc-native-cpp-26-online", + "configuration": "Release", + "targets": [ + "build", + "run_unittests", + "run_examples" + ] + }, + { + "name": "build-Release-gcc-native-cpp-26-offline", + "configurePreset": "configure-gcc-native-cpp-26-offline", + "configuration": "Release", + "targets": [ + "build", + "run_unittests", + "run_examples" + ] + }, + { + "name": "build-Release-gcc-native-32-cpp-26-online", + "configurePreset": "configure-gcc-native-32-cpp-26-online", + "configuration": "Release", + "targets": [ + "build", + "run_unittests", + "run_examples" + ] + }, + { + "name": "build-Release-gcc-native-32-cpp-26-offline", + "configurePreset": "configure-gcc-native-32-cpp-26-offline", + "configuration": "Release", + "targets": [ + "build", + "run_unittests", + "run_examples" + ] + }, + { + "name": "build-Release-clang-native-cpp-26-online", + "configurePreset": "configure-clang-native-cpp-26-online", + "configuration": "Release", + "targets": [ + "build", + "run_unittests", + "run_examples" + ] + }, + { + "name": "build-Release-clang-native-cpp-26-offline", + "configurePreset": "configure-clang-native-cpp-26-offline", + "configuration": "Release", + "targets": [ + "build", + "run_unittests", + "run_examples" + ] + }, + { + "name": "build-ReleaseEP-gcc-native-cpp-26-online", + "configurePreset": "configure-gcc-native-cpp-26-online", + "configuration": "ReleaseEP", + "targets": [ + "build", + "run_unittests", + "run_examples" + ] + }, + { + "name": "build-ReleaseEP-gcc-native-cpp-26-offline", + "configurePreset": "configure-gcc-native-cpp-26-offline", + "configuration": "ReleaseEP", + "targets": [ + "build", + "run_unittests", + "run_examples" + ] + }, + { + "name": "build-ReleaseEP-gcc-native-32-cpp-26-online", + "configurePreset": "configure-gcc-native-32-cpp-26-online", + "configuration": "ReleaseEP", + "targets": [ + "build", + "run_unittests", + "run_examples" + ] + }, + { + "name": "build-ReleaseEP-gcc-native-32-cpp-26-offline", + "configurePreset": "configure-gcc-native-32-cpp-26-offline", + "configuration": "ReleaseEP", + "targets": [ + "build", + "run_unittests", + "run_examples" + ] + }, + { + "name": "build-ReleaseEP-clang-native-cpp-26-online", + "configurePreset": "configure-clang-native-cpp-26-online", + "configuration": "ReleaseEP", + "targets": [ + "build", + "run_unittests", + "run_examples" + ] + }, + { + "name": "build-ReleaseEP-clang-native-cpp-26-offline", + "configurePreset": "configure-clang-native-cpp-26-offline", + "configuration": "ReleaseEP", + "targets": [ + "build", + "run_unittests", + "run_examples" + ] + }, + { + "name": "build-ReleaseEP-arm-none-eabi-gcc-cpp-26-online", + "configurePreset": "configure-arm-none-eabi-gcc-cpp-26-online", + "configuration": "ReleaseEP", + "targets": [ + "build", + "run_unittests", + "run_examples" + ] + }, + { + "name": "build-ReleaseEP-arm-none-eabi-gcc-cpp-26-offline", + "configurePreset": "configure-arm-none-eabi-gcc-cpp-26-offline", + "configuration": "ReleaseEP", + "targets": [ + "build", + "run_unittests", + "run_examples" + ] + }, + { + "name": "build-Debug-gcc-native-cpp-26-online", + "configurePreset": "configure-gcc-native-cpp-26-online", + "configuration": "Debug", + "targets": [ + "build", + "run_unittests", + "run_examples" + ] + }, + { + "name": "build-Debug-gcc-native-cpp-26-offline", + "configurePreset": "configure-gcc-native-cpp-26-offline", + "configuration": "Debug", + "targets": [ + "build", + "run_unittests", + "run_examples" + ] + }, + { + "name": "build-Debug-gcc-native-32-cpp-26-online", + "configurePreset": "configure-gcc-native-32-cpp-26-online", + "configuration": "Debug", + "targets": [ + "build", + "run_unittests", + "run_examples" + ] + }, + { + "name": "build-Debug-gcc-native-32-cpp-26-offline", + "configurePreset": "configure-gcc-native-32-cpp-26-offline", + "configuration": "Debug", + "targets": [ + "build", + "run_unittests", + "run_examples" + ] + }, + { + "name": "build-Debug-clang-native-cpp-26-online", + "configurePreset": "configure-clang-native-cpp-26-online", + "configuration": "Debug", + "targets": [ + "build", + "run_unittests", + "run_examples" + ] + }, + { + "name": "build-Debug-clang-native-cpp-26-offline", + "configurePreset": "configure-clang-native-cpp-26-offline", + "configuration": "Debug", + "targets": [ + "build", + "run_unittests", + "run_examples" + ] + }, + { + "name": "build-DebugEP-gcc-native-cpp-26-online", + "configurePreset": "configure-gcc-native-cpp-26-online", + "configuration": "DebugEP", + "targets": [ + "build", + "run_unittests", + "run_examples" + ] + }, + { + "name": "build-DebugEP-gcc-native-cpp-26-offline", + "configurePreset": "configure-gcc-native-cpp-26-offline", + "configuration": "DebugEP", + "targets": [ + "build", + "run_unittests", + "run_examples" + ] + }, + { + "name": "build-DebugEP-gcc-native-32-cpp-26-online", + "configurePreset": "configure-gcc-native-32-cpp-26-online", + "configuration": "DebugEP", + "targets": [ + "build", + "run_unittests", + "run_examples" + ] + }, + { + "name": "build-DebugEP-gcc-native-32-cpp-26-offline", + "configurePreset": "configure-gcc-native-32-cpp-26-offline", + "configuration": "DebugEP", + "targets": [ + "build", + "run_unittests", + "run_examples" + ] + }, + { + "name": "build-DebugEP-clang-native-cpp-26-online", + "configurePreset": "configure-clang-native-cpp-26-online", + "configuration": "DebugEP", + "targets": [ + "build", + "run_unittests", + "run_examples" + ] + }, + { + "name": "build-DebugEP-clang-native-cpp-26-offline", + "configurePreset": "configure-clang-native-cpp-26-offline", + "configuration": "DebugEP", + "targets": [ + "build", + "run_unittests", + "run_examples" + ] + }, + { + "name": "build-DebugEP-arm-none-eabi-gcc-cpp-26-online", + "configurePreset": "configure-arm-none-eabi-gcc-cpp-26-online", + "configuration": "DebugEP", + "targets": [ + "build", + "run_unittests", + "run_examples" + ] + }, + { + "name": "build-DebugEP-arm-none-eabi-gcc-cpp-26-offline", + "configurePreset": "configure-arm-none-eabi-gcc-cpp-26-offline", + "configuration": "DebugEP", + "targets": [ + "build", + "run_unittests", + "run_examples" + ] + }, + { + "name": "build-Coverage-gcc-native-cpp-26-online", + "configurePreset": "configure-gcc-native-cpp-26-online", + "configuration": "Coverage", + "targets": [ + "build", + "run_unittests", + "gcovr_html_report_for_unittest", + "run_examples", + "gcovr_html_report_for_examples" + ] + }, + { + "name": "build-Coverage-gcc-native-cpp-26-offline", + "configurePreset": "configure-gcc-native-cpp-26-offline", + "configuration": "Coverage", + "targets": [ + "build", + "run_unittests", + "gcovr_html_report_for_unittest", + "run_examples", + "gcovr_html_report_for_examples" + ] + }, + { + "name": "build-Coverage-gcc-native-32-cpp-26-online", + "configurePreset": "configure-gcc-native-32-cpp-26-online", + "configuration": "Coverage", + "targets": [ + "build", + "run_unittests", + "gcovr_html_report_for_unittest", + "run_examples", + "gcovr_html_report_for_examples" + ] + }, + { + "name": "build-Coverage-gcc-native-32-cpp-26-offline", + "configurePreset": "configure-gcc-native-32-cpp-26-offline", + "configuration": "Coverage", + "targets": [ + "build", + "run_unittests", + "gcovr_html_report_for_unittest", + "run_examples", + "gcovr_html_report_for_examples" + ] + }, + { + "name": "build-Coverage-clang-native-cpp-26-online", + "configurePreset": "configure-clang-native-cpp-26-online", + "configuration": "Coverage", + "targets": [ + "build", + "run_unittests", + "gcovr_html_report_for_unittest", + "run_examples", + "gcovr_html_report_for_examples" + ] + }, + { + "name": "build-Coverage-clang-native-cpp-26-offline", + "configurePreset": "configure-clang-native-cpp-26-offline", + "configuration": "Coverage", + "targets": [ + "build", + "run_unittests", + "gcovr_html_report_for_unittest", + "run_examples", + "gcovr_html_report_for_examples" + ] } ], "workflowPresets": [ @@ -2927,6 +3358,206 @@ "name": "build-DebugEP-arm-none-eabi-gcc-cpp-23-offline" } ] + }, + { + "name": "workflow-gcc-native-cpp-26-online", + "displayName": "gcc native cpp 26 online", + "description": "autogenerated workflow", + "steps": [ + { + "type": "configure", + "name": "configure-gcc-native-cpp-26-online" + }, + { + "type": "build", + "name": "build-Release-gcc-native-cpp-26-online" + }, + { + "type": "build", + "name": "build-ReleaseEP-gcc-native-cpp-26-online" + }, + { + "type": "build", + "name": "build-Debug-gcc-native-cpp-26-online" + }, + { + "type": "build", + "name": "build-DebugEP-gcc-native-cpp-26-online" + } + ] + }, + { + "name": "workflow-gcc-native-cpp-26-offline", + "displayName": "gcc native cpp 26 offline", + "description": "autogenerated workflow", + "steps": [ + { + "type": "configure", + "name": "configure-gcc-native-cpp-26-offline" + }, + { + "type": "build", + "name": "build-Release-gcc-native-cpp-26-offline" + }, + { + "type": "build", + "name": "build-ReleaseEP-gcc-native-cpp-26-offline" + }, + { + "type": "build", + "name": "build-Debug-gcc-native-cpp-26-offline" + }, + { + "type": "build", + "name": "build-DebugEP-gcc-native-cpp-26-offline" + } + ] + }, + { + "name": "workflow-gcc-native-32-cpp-26-online", + "displayName": "gcc native 32 cpp 26 online", + "description": "autogenerated workflow", + "steps": [ + { + "type": "configure", + "name": "configure-gcc-native-32-cpp-26-online" + }, + { + "type": "build", + "name": "build-Release-gcc-native-32-cpp-26-online" + }, + { + "type": "build", + "name": "build-ReleaseEP-gcc-native-32-cpp-26-online" + }, + { + "type": "build", + "name": "build-Debug-gcc-native-32-cpp-26-online" + }, + { + "type": "build", + "name": "build-DebugEP-gcc-native-32-cpp-26-online" + } + ] + }, + { + "name": "workflow-gcc-native-32-cpp-26-offline", + "displayName": "gcc native 32 cpp 26 offline", + "description": "autogenerated workflow", + "steps": [ + { + "type": "configure", + "name": "configure-gcc-native-32-cpp-26-offline" + }, + { + "type": "build", + "name": "build-Release-gcc-native-32-cpp-26-offline" + }, + { + "type": "build", + "name": "build-ReleaseEP-gcc-native-32-cpp-26-offline" + }, + { + "type": "build", + "name": "build-Debug-gcc-native-32-cpp-26-offline" + }, + { + "type": "build", + "name": "build-DebugEP-gcc-native-32-cpp-26-offline" + } + ] + }, + { + "name": "workflow-clang-native-cpp-26-online", + "displayName": "clang native cpp 26 online", + "description": "autogenerated workflow", + "steps": [ + { + "type": "configure", + "name": "configure-clang-native-cpp-26-online" + }, + { + "type": "build", + "name": "build-Release-clang-native-cpp-26-online" + }, + { + "type": "build", + "name": "build-ReleaseEP-clang-native-cpp-26-online" + }, + { + "type": "build", + "name": "build-Debug-clang-native-cpp-26-online" + }, + { + "type": "build", + "name": "build-DebugEP-clang-native-cpp-26-online" + } + ] + }, + { + "name": "workflow-clang-native-cpp-26-offline", + "displayName": "clang native cpp 26 offline", + "description": "autogenerated workflow", + "steps": [ + { + "type": "configure", + "name": "configure-clang-native-cpp-26-offline" + }, + { + "type": "build", + "name": "build-Release-clang-native-cpp-26-offline" + }, + { + "type": "build", + "name": "build-ReleaseEP-clang-native-cpp-26-offline" + }, + { + "type": "build", + "name": "build-Debug-clang-native-cpp-26-offline" + }, + { + "type": "build", + "name": "build-DebugEP-clang-native-cpp-26-offline" + } + ] + }, + { + "name": "workflow-arm-none-eabi-gcc-cpp-26-online", + "displayName": "arm none eabi gcc cpp 26 online", + "description": "autogenerated workflow", + "steps": [ + { + "type": "configure", + "name": "configure-arm-none-eabi-gcc-cpp-26-online" + }, + { + "type": "build", + "name": "build-ReleaseEP-arm-none-eabi-gcc-cpp-26-online" + }, + { + "type": "build", + "name": "build-DebugEP-arm-none-eabi-gcc-cpp-26-online" + } + ] + }, + { + "name": "workflow-arm-none-eabi-gcc-cpp-26-offline", + "displayName": "arm none eabi gcc cpp 26 offline", + "description": "autogenerated workflow", + "steps": [ + { + "type": "configure", + "name": "configure-arm-none-eabi-gcc-cpp-26-offline" + }, + { + "type": "build", + "name": "build-ReleaseEP-arm-none-eabi-gcc-cpp-26-offline" + }, + { + "type": "build", + "name": "build-DebugEP-arm-none-eabi-gcc-cpp-26-offline" + } + ] } ], "packagePresets": [], @@ -2991,7 +3622,8 @@ "cpp-14", "cpp-17", "cpp-20", - "cpp-23" + "cpp-23", + "cpp-26" ], "update": [ "online", @@ -3004,7 +3636,8 @@ "standard": [ "cpp-17", "cpp-20", - "cpp-23" + "cpp-23", + "cpp-26" ] } ], @@ -3048,7 +3681,8 @@ "cpp-14", "cpp-17", "cpp-20", - "cpp-23" + "cpp-23", + "cpp-26" ], "update": [ "online", @@ -3061,7 +3695,8 @@ "standard": [ "cpp-17", "cpp-20", - "cpp-23" + "cpp-23", + "cpp-26" ] }, { @@ -3098,7 +3733,8 @@ "cpp-14", "cpp-17", "cpp-20", - "cpp-23" + "cpp-23", + "cpp-26" ], "update": [ "online", @@ -3120,7 +3756,8 @@ "standard": [ "cpp-17", "cpp-20", - "cpp-23" + "cpp-23", + "cpp-26" ] }, { diff --git a/cetlvast/CMakePresetsVendorTemplate.json b/cetlvast/CMakePresetsVendorTemplate.json index 720a95c9..e6106d83 100644 --- a/cetlvast/CMakePresetsVendorTemplate.json +++ b/cetlvast/CMakePresetsVendorTemplate.json @@ -58,7 +58,8 @@ "cpp-14", "cpp-17", "cpp-20", - "cpp-23" + "cpp-23", + "cpp-26" ], "update": [ "online", @@ -68,7 +69,7 @@ "exclude": [ { "toolchain": "gcc7-native", - "standard": ["cpp-17", "cpp-20", "cpp-23"] + "standard": ["cpp-17", "cpp-20", "cpp-23", "cpp-26"] } ], "shape": { @@ -101,7 +102,7 @@ "exclude": [ { "toolchain": "gcc7-native", - "standard": ["cpp-17", "cpp-20", "cpp-23"] + "standard": ["cpp-17", "cpp-20", "cpp-23", "cpp-26"] }, { "toolchain": "arm-none-eabi-gcc", @@ -129,7 +130,7 @@ "exclude": [ { "toolchain": "gcc7-native", - "standard": ["cpp-17", "cpp-20", "cpp-23"] + "standard": ["cpp-17", "cpp-20", "cpp-23", "cpp-26"] }, { "configuration": "Coverage" diff --git a/cetlvast/suites/compile/CMakeLists.txt b/cetlvast/suites/compile/CMakeLists.txt index 1f8a7a57..3b82b1cf 100644 --- a/cetlvast/suites/compile/CMakeLists.txt +++ b/cetlvast/suites/compile/CMakeLists.txt @@ -11,6 +11,9 @@ include(CompileTest) set(COMPILE_TESTS ${CMAKE_CURRENT_SOURCE_DIR}/test_cetl_h_erase.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/test_pf20_cmp_less_bool.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/test_pf26_saturating_add_bool.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/test_pf26_saturating_add_mixed_types.cpp ${CMAKE_CURRENT_SOURCE_DIR}/test_span_empty.cpp ${CMAKE_CURRENT_SOURCE_DIR}/test_unbounded_variant_footprint_get_const.cpp ${CMAKE_CURRENT_SOURCE_DIR}/test_unbounded_variant_footprint_get_non_const.cpp diff --git a/cetlvast/suites/compile/test_pf20_cmp_less_bool.cpp b/cetlvast/suites/compile/test_pf20_cmp_less_bool.cpp new file mode 100644 index 00000000..7ab7df61 --- /dev/null +++ b/cetlvast/suites/compile/test_pf20_cmp_less_bool.cpp @@ -0,0 +1,19 @@ +/// @file +/// Compile test that ensures bool is not accepted by the integer comparison functions, as in C++20. +/// +/// @copyright +/// Copyright (C) OpenCyphal Development Team +/// Copyright Amazon.com Inc. or its affiliates. +/// SPDX-License-Identifier: MIT +/// +#include "cetl/pf20/utility.hpp" + +int main() +{ +#ifndef CETLVAST_COMPILETEST_PRECHECK + // Mandated: bool is not a standard or extended integer type, so this fails a static assertion. + return cetl::pf20::cmp_less(false, 1) ? 0 : 1; +#else + return cetl::pf20::cmp_less(0, 1) ? 0 : 1; +#endif +} diff --git a/cetlvast/suites/compile/test_pf26_saturating_add_bool.cpp b/cetlvast/suites/compile/test_pf26_saturating_add_bool.cpp new file mode 100644 index 00000000..8d0d9a3f --- /dev/null +++ b/cetlvast/suites/compile/test_pf26_saturating_add_bool.cpp @@ -0,0 +1,19 @@ +/// @file +/// Compile test that ensures bool is not accepted by the saturation arithmetic functions, as in C++26. +/// +/// @copyright +/// Copyright (C) OpenCyphal Development Team +/// Copyright Amazon.com Inc. or its affiliates. +/// SPDX-License-Identifier: MIT +/// +#include "cetl/pf26/numeric.hpp" + +int main() +{ +#ifndef CETLVAST_COMPILETEST_PRECHECK + // bool is not a signed or unsigned integer type: no matching function. + return cetl::pf26::saturating_add(true, true) ? 0 : 1; +#else + return cetl::pf26::saturating_add(1, 1) - 2; +#endif +} diff --git a/cetlvast/suites/compile/test_pf26_saturating_add_mixed_types.cpp b/cetlvast/suites/compile/test_pf26_saturating_add_mixed_types.cpp new file mode 100644 index 00000000..9c0b952d --- /dev/null +++ b/cetlvast/suites/compile/test_pf26_saturating_add_mixed_types.cpp @@ -0,0 +1,19 @@ +/// @file +/// Compile test that ensures operands of different types are rejected rather than promoted, as in C++26. +/// +/// @copyright +/// Copyright (C) OpenCyphal Development Team +/// Copyright Amazon.com Inc. or its affiliates. +/// SPDX-License-Identifier: MIT +/// +#include "cetl/pf26/numeric.hpp" + +int main() +{ +#ifndef CETLVAST_COMPILETEST_PRECHECK + // Deduction of T from int and long conflicts, exactly as for std::min(1, 1L). + return static_cast(cetl::pf26::saturating_add(1, 1L)) - 2; +#else + return static_cast(cetl::pf26::saturating_add(1, 1L)) - 2; +#endif +} diff --git a/cetlvast/suites/docs/examples/CMakeLists.txt b/cetlvast/suites/docs/examples/CMakeLists.txt index c9ebfbf3..d43755bc 100644 --- a/cetlvast/suites/docs/examples/CMakeLists.txt +++ b/cetlvast/suites/docs/examples/CMakeLists.txt @@ -33,6 +33,7 @@ set(NATIVE_EXAMPLES example_10_unbounded_variant.cpp example_11_memory_resource_o1heap.cpp example_12_buddy_memory_resource.cpp + example_13_saturating_arithmetic.cpp ) set(ALL_EXAMPLES "") diff --git a/cetlvast/suites/docs/examples/example_13_saturating_arithmetic.cpp b/cetlvast/suites/docs/examples/example_13_saturating_arithmetic.cpp new file mode 100644 index 00000000..e351d9bd --- /dev/null +++ b/cetlvast/suites/docs/examples/example_13_saturating_arithmetic.cpp @@ -0,0 +1,71 @@ +/// @file +/// Example of using the CETL C++26 polyfill header for saturation arithmetic. +/// +/// @copyright +/// Copyright (C) OpenCyphal Development Team +/// Copyright Amazon.com Inc. or its affiliates. +/// SPDX-License-Identifier: MIT +/// +#include "cetl/pf26/cetlpf.hpp" +#include "cetl/pf17/cetlpf.hpp" + +#include +#include +#include +#include + +#include + +TEST(example_13_saturating_arithmetic, saturating_add) +{ + //! [example_13_saturating_add] + // A drop counter that must stop at its maximum instead of wrapping back to zero. + // The cetl/pf26/cetlpf.hpp header provides cetl::saturating_add from the standard library when it has it + // (under either the 2023 or the final 2026 names) and from cetl::pf26 otherwise. + std::uint16_t dropped = 65530U; + for (int i = 0; i < 10; ++i) + { + dropped = cetl::saturating_add(dropped, std::uint16_t{1}); + } + std::cout << "dropped: " << dropped << std::endl; // 65535, not 4 + //! [example_13_saturating_add] + ASSERT_EQ(65535U, dropped); +} + +TEST(example_13_saturating_arithmetic, saturating_cast) +{ + //! [example_13_saturating_cast] + // Uptime in seconds is a 32-bit field on the wire (for example uavcan.node.Heartbeat). A static_cast would + // wrap after 136 years; saturating_cast pins the value at the field's maximum instead. + const std::chrono::seconds uptime{INT64_C(5000000000)}; + const std::uint32_t field = cetl::saturating_cast(uptime.count()); + std::cout << "uptime field: " << field << std::endl; // 4294967295 + //! [example_13_saturating_cast] + ASSERT_EQ(std::numeric_limits::max(), field); +} + +TEST(example_13_saturating_arithmetic, operands_share_one_type) +{ + //! [example_13_one_type] + // As with std::min and std::max, both operands share one type and nothing is promoted. Name the type when the + // operands differ; otherwise the call does not compile. + const std::int8_t a = 100; + const std::int8_t sum = cetl::saturating_add(a, 100); // 127 + //! [example_13_one_type] + ASSERT_EQ(127, sum); +} + +TEST(example_13_saturating_arithmetic, clamp_and_in_range) +{ + //! [example_13_clamp] + // A value destined for a 10-bit signed field (DSDL `saturated int10`) has no matching fundamental type, so it is + // clamped to the field's range with std::clamp, which cetl/pf17/cetlpf.hpp provides for C++14... + const std::int16_t raw = 0x5555; + const std::int16_t field = cetl::clamp(raw, std::int16_t{-512}, std::int16_t{511}); // 511 + // ...and std::in_range (cetl/pf20/cetlpf.hpp) answers "would saturating_cast change this value?" without + // performing the conversion. + const bool fits = cetl::in_range(raw); // false + //! [example_13_clamp] + ASSERT_EQ(511, field); + ASSERT_FALSE(fits); +} diff --git a/cetlvast/suites/fuzz/CMakeLists.txt b/cetlvast/suites/fuzz/CMakeLists.txt index 2e7dd041..d647ea00 100644 --- a/cetlvast/suites/fuzz/CMakeLists.txt +++ b/cetlvast/suites/fuzz/CMakeLists.txt @@ -35,24 +35,33 @@ if(NOT CETLVAST_HAS_LIBFUZZER) message(FATAL_ERROR "Clang cannot link -fsanitize=fuzzer,address,undefined; install the compiler-rt runtimes.") endif() -add_executable(fuzz_variable_length_array fuzz_variable_length_array.cpp) -target_link_libraries(fuzz_variable_length_array PRIVATE cetl) -target_compile_options(fuzz_variable_length_array PRIVATE - -O1 -g -fno-omit-frame-pointer -fsanitize=fuzzer,address,undefined -fno-sanitize-recover=all - -Wall -Wextra -Wpedantic -Werror) -target_link_options(fuzz_variable_length_array PRIVATE - -fsanitize=fuzzer,address,undefined -fno-sanitize-recover=all) - -# Keep generated inputs and crash artifacts in the build tree, leaving the seed corpus unchanged. -set(FUZZ_WORK_DIR "${CMAKE_CURRENT_BINARY_DIR}/variable_length_array") -file(MAKE_DIRECTORY "${FUZZ_WORK_DIR}/corpus" "${FUZZ_WORK_DIR}/artifacts") -set(FUZZ_SMOKE_ARGS - "${FUZZ_WORK_DIR}/corpus" "${CMAKE_CURRENT_SOURCE_DIR}/corpus/variable_length_array" - -runs=10000 -seed=1 -max_len=1024 -timeout=10 - "-artifact_prefix=${FUZZ_WORK_DIR}/artifacts/") -add_test(NAME fuzz_variable_length_array_smoke COMMAND fuzz_variable_length_array ${FUZZ_SMOKE_ARGS}) -set_tests_properties(fuzz_variable_length_array_smoke PROPERTIES LABELS fuzz TIMEOUT 120) +# Each target is a libFuzzer entry point with a seed corpus in corpus/; generated inputs and crash artifacts +# stay in the build tree so the seed corpus is left unchanged. +set(FUZZ_TARGETS + variable_length_array + saturation_arithmetic +) +set(FUZZ_SMOKE_COMMANDS "") +foreach(FUZZ_NAME ${FUZZ_TARGETS}) + set(FUZZ_TARGET "fuzz_${FUZZ_NAME}") + add_executable(${FUZZ_TARGET} ${FUZZ_TARGET}.cpp) + target_link_libraries(${FUZZ_TARGET} PRIVATE cetl) + target_compile_options(${FUZZ_TARGET} PRIVATE + -O1 -g -fno-omit-frame-pointer -fsanitize=fuzzer,address,undefined -fno-sanitize-recover=all + -Wall -Wextra -Wpedantic -Werror) + target_link_options(${FUZZ_TARGET} PRIVATE + -fsanitize=fuzzer,address,undefined -fno-sanitize-recover=all) + set(FUZZ_WORK_DIR "${CMAKE_CURRENT_BINARY_DIR}/${FUZZ_NAME}") + file(MAKE_DIRECTORY "${FUZZ_WORK_DIR}/corpus" "${FUZZ_WORK_DIR}/artifacts") + set(FUZZ_SMOKE_ARGS + "${FUZZ_WORK_DIR}/corpus" "${CMAKE_CURRENT_SOURCE_DIR}/corpus/${FUZZ_NAME}" + -runs=10000 -seed=1 -max_len=1024 -timeout=10 + "-artifact_prefix=${FUZZ_WORK_DIR}/artifacts/") + add_test(NAME ${FUZZ_TARGET}_smoke COMMAND ${FUZZ_TARGET} ${FUZZ_SMOKE_ARGS}) + set_tests_properties(${FUZZ_TARGET}_smoke PROPERTIES LABELS fuzz TIMEOUT 120) + list(APPEND FUZZ_SMOKE_COMMANDS COMMAND $ ${FUZZ_SMOKE_ARGS}) +endforeach() add_custom_target(run_fuzztests - COMMAND $ ${FUZZ_SMOKE_ARGS} - DEPENDS fuzz_variable_length_array + ${FUZZ_SMOKE_COMMANDS} + DEPENDS fuzz_variable_length_array fuzz_saturation_arithmetic USES_TERMINAL VERBATIM) diff --git a/cetlvast/suites/fuzz/corpus/saturation_arithmetic/int32_min_div_neg1 b/cetlvast/suites/fuzz/corpus/saturation_arithmetic/int32_min_div_neg1 new file mode 100644 index 0000000000000000000000000000000000000000..c80321b80b45978513b93f985acc435711f3357a GIT binary patch literal 17 ScmZQ!U|?tfVut@f0HOgPy99v% literal 0 HcmV?d00001 diff --git a/cetlvast/suites/fuzz/corpus/saturation_arithmetic/int64_min_times_neg1 b/cetlvast/suites/fuzz/corpus/saturation_arithmetic/int64_min_times_neg1 new file mode 100644 index 0000000000000000000000000000000000000000..5d8d55ea75c3e85800d7ddfe1b34eb1ee0bd95bc GIT binary patch literal 17 OcmZQ$fPjYoPyhfZ(+Gb6 literal 0 HcmV?d00001 diff --git a/cetlvast/suites/fuzz/corpus/saturation_arithmetic/int8_min_minus_one b/cetlvast/suites/fuzz/corpus/saturation_arithmetic/int8_min_minus_one new file mode 100644 index 0000000000000000000000000000000000000000..9c58e7dd4851247cbafb3fb8986949add0ab779e GIT binary patch literal 17 OcmZQ*V1R)CP#ORb2?2lr literal 0 HcmV?d00001 diff --git a/cetlvast/suites/fuzz/corpus/saturation_arithmetic/uint64_max_times_two b/cetlvast/suites/fuzz/corpus/saturation_arithmetic/uint64_max_times_two new file mode 100644 index 0000000000000000000000000000000000000000..a4241b8dcec80752c6461b793f702c5fbc0bd645 GIT binary patch literal 17 OcmZSo4+TsN5C8yVGzbC! literal 0 HcmV?d00001 diff --git a/cetlvast/suites/fuzz/corpus/saturation_arithmetic/uint8_max_plus_one b/cetlvast/suites/fuzz/corpus/saturation_arithmetic/uint8_max_plus_one new file mode 100644 index 0000000000000000000000000000000000000000..66455be81a0d7d6a82cb1a1d30aadf209b57a2b7 GIT binary patch literal 17 OcmZSl&j108P#ORb8UX?T literal 0 HcmV?d00001 diff --git a/cetlvast/suites/fuzz/fuzz_saturation_arithmetic.cpp b/cetlvast/suites/fuzz/fuzz_saturation_arithmetic.cpp new file mode 100644 index 00000000..036f569d --- /dev/null +++ b/cetlvast/suites/fuzz/fuzz_saturation_arithmetic.cpp @@ -0,0 +1,139 @@ +/// @file +/// Differential fuzz test: the cetl::pf26 saturation functions (both the builtin-backed and the portable +/// implementations) against a 128-bit oracle, for every fixed-width integer type. +/// +/// Input: byte 0 selects the operand type (low three bits); bytes 1..16 are the two operands. +/// +/// @copyright +/// Copyright (C) OpenCyphal Development Team +/// SPDX-License-Identifier: MIT + +#include "cetl/pf26/numeric.hpp" + +#include +#include +#include +#include +#include +#include + +__extension__ typedef __int128 fuzz_int128; +__extension__ typedef unsigned __int128 fuzz_uint128; + +namespace +{ + +template +using wide_t = typename std::conditional::value, fuzz_int128, fuzz_uint128>::type; + +template +T clamp_wide(fuzz_int128 v) +{ + const fuzz_int128 lo = std::numeric_limits::min(); + const fuzz_int128 hi = std::numeric_limits::max(); + return static_cast(std::min(std::max(v, lo), hi)); +} + +template +void require(bool condition, const char* what, T x, T y) +{ + if (!condition) + { + std::fprintf(stderr, + "saturation fuzz invariant failed: %s (%zu-byte %s operands %lld, %lld)\n", + what, + sizeof(T), + std::is_signed::value ? "signed" : "unsigned", + static_cast(x), + static_cast(y)); + __builtin_trap(); + } +} + +template +void check_cast(From x) +{ + const fuzz_int128 expected = clamp_wide(static_cast(x)); + require(static_cast(cetl::pf26::saturating_cast(x)) == expected, "saturating_cast", x, x); +} + +template +void check(const std::uint8_t* const operands) +{ + T x{}; + T y{}; + std::memcpy(&x, operands, sizeof(T)); + std::memcpy(&y, operands + 8, sizeof(T)); + + const fuzz_int128 a = x; + const fuzz_int128 b = y; + const T sum = clamp_wide(a + b); + require(cetl::pf26::saturating_add(x, y) == sum, "saturating_add", x, y); + require(cetl::pf26::detail::saturating_add_portable(x, y) == sum, "saturating_add_portable", x, y); + + const T difference = clamp_wide(a - b); + require(cetl::pf26::saturating_sub(x, y) == difference, "saturating_sub", x, y); + require(cetl::pf26::detail::saturating_sub_portable(x, y) == difference, "saturating_sub_portable", x, y); + + // The product of two 64-bit values fits in the 128-bit type of the same signedness. + const wide_t product_wide = static_cast>(x) * static_cast>(y); + const T product = std::is_signed::value + ? clamp_wide(static_cast(product_wide)) + : static_cast(std::min(static_cast(product_wide), + static_cast(std::numeric_limits::max()))); + require(cetl::pf26::saturating_mul(x, y) == product, "saturating_mul", x, y); + require(cetl::pf26::detail::saturating_mul_portable(x, y) == product, "saturating_mul_portable", x, y); + + if (y != 0) + { + require(cetl::pf26::saturating_div(x, y) == clamp_wide(a / b), "saturating_div", x, y); + } + + check_cast(x); + check_cast(x); + check_cast(x); + check_cast(x); + check_cast(x); + check_cast(x); + check_cast(x); + check_cast(x); +} + +} // namespace + +extern "C" int LLVMFuzzerTestOneInput(const std::uint8_t* data, std::size_t size) +{ + if (size < 17) + { + return 0; + } + const std::uint8_t* const operands = data + 1; + switch (data[0] & 7U) + { + case 0: + check(operands); + break; + case 1: + check(operands); + break; + case 2: + check(operands); + break; + case 3: + check(operands); + break; + case 4: + check(operands); + break; + case 5: + check(operands); + break; + case 6: + check(operands); + break; + default: + check(operands); + break; + } + return 0; +} diff --git a/cetlvast/suites/unittest/CMakeLists.txt b/cetlvast/suites/unittest/CMakeLists.txt index 9efa983c..32c64a19 100644 --- a/cetlvast/suites/unittest/CMakeLists.txt +++ b/cetlvast/suites/unittest/CMakeLists.txt @@ -86,6 +86,7 @@ set(NATIVE_TESTS test_buffer_memory_resource.cpp test_deprecated_names.cpp test_o1heap_memory_resource.cpp + test_pf17_algorithm.cpp test_pf17_new.cpp test_pf17_memory_resource.cpp test_pf17_monotonic_buffer_resource.cpp @@ -108,6 +109,8 @@ set(NATIVE_TESTS test_pf20_span_asserts.cpp test_pf20_span.cpp test_pf20.cpp + test_pf20_utility.cpp + test_pf26_numeric.cpp test_rtti.cpp test_type_traits_ext.cpp test_typelist.cpp diff --git a/cetlvast/suites/unittest/test_pf17_algorithm.cpp b/cetlvast/suites/unittest/test_pf17_algorithm.cpp new file mode 100644 index 00000000..dce65bb3 --- /dev/null +++ b/cetlvast/suites/unittest/test_pf17_algorithm.cpp @@ -0,0 +1,94 @@ +/// @file +/// Unit tests for cetl/pf17/algorithm.hpp +/// +/// @copyright +/// Copyright (C) OpenCyphal Development Team +/// Copyright Amazon.com Inc. or its affiliates. +/// SPDX-License-Identifier: MIT + +#include "cetlvast/helpers_gtest.hpp" +#include "cetl/pf17/algorithm.hpp" +#include "cetl/pf17/cetlpf.hpp" + +#include +#include + +namespace +{ + +// +----------------------------------------------------------------------+ +// | Constant expressions +// +----------------------------------------------------------------------+ +static_assert(cetl::pf17::clamp(5, 1, 3) == 3, "above hi"); +static_assert(cetl::pf17::clamp(-5, 1, 3) == 1, "below lo"); +static_assert(cetl::pf17::clamp(2, 1, 3) == 2, "inside"); +static_assert(cetl::pf17::clamp(1, 1, 3) == 1, "at lo"); +static_assert(cetl::pf17::clamp(3, 1, 3) == 3, "at hi"); +static_assert(cetl::pf17::clamp(2, 2, 2) == 2, "degenerate range"); +static_assert(cetl::pf17::clamp(5, 3, 1, std::greater{}) == 3, "reversed comparison, above"); +static_assert(cetl::pf17::clamp(0, 3, 1, std::greater{}) == 1, "reversed comparison, below"); +static_assert(cetl::pf17::clamp(2, 3, 1, std::greater{}) == 2, "reversed comparison, inside"); +static_assert(cetl::clamp(5, 1, 3) == 3, "cetlpf alias"); + +// +----------------------------------------------------------------------+ +// | Reference semantics +// +----------------------------------------------------------------------+ +TEST(PF17AlgorithmTest, ClampReturnsReferenceToArgument) +{ + const int v = 2; + const int lo = 1; + const int hi = 3; + const int below = 0; + const int above = 9; + EXPECT_EQ(&v, &cetl::pf17::clamp(v, lo, hi)); + EXPECT_EQ(&lo, &cetl::pf17::clamp(below, lo, hi)); + EXPECT_EQ(&hi, &cetl::pf17::clamp(above, lo, hi)); + // At a bound, v itself is returned (it is not less than lo and hi is not less than it). + EXPECT_EQ(&lo, &cetl::pf17::clamp(lo, lo, hi)); + EXPECT_NE(&lo, &cetl::pf17::clamp(v, v, hi)); + EXPECT_EQ(&v, &cetl::pf17::clamp(v, v, hi)); +} + +TEST(PF17AlgorithmTest, ClampWithComparator) +{ + const auto by_length = [](const std::string& a, const std::string& b) { return a.size() < b.size(); }; + const std::string lo{"ab"}; + const std::string hi{"abcd"}; + const std::string too_long{"abcdef"}; + const std::string too_short{"a"}; + const std::string inside{"abc"}; + EXPECT_EQ(hi, cetl::pf17::clamp(too_long, lo, hi, by_length)); + EXPECT_EQ(lo, cetl::pf17::clamp(too_short, lo, hi, by_length)); + EXPECT_EQ(inside, cetl::pf17::clamp(inside, lo, hi, by_length)); +} + +TEST(PF17AlgorithmTest, ClampFloatingPoint) +{ + EXPECT_DOUBLE_EQ(1.5, cetl::pf17::clamp(1.5, 1.0, 2.0)); + EXPECT_DOUBLE_EQ(1.0, cetl::pf17::clamp(-1.5, 1.0, 2.0)); + EXPECT_DOUBLE_EQ(2.0, cetl::pf17::clamp(100.0, 1.0, 2.0)); +} + +// +----------------------------------------------------------------------+ +// | Precondition +// +----------------------------------------------------------------------+ +#if defined(CETL_ENABLE_DEBUG_ASSERT) && CETL_ENABLE_DEBUG_ASSERT + +static void TestClampWithInvertedBounds() +{ + flush_coverage_on_death(); + const int lo = 3; + const int hi = 1; + const int v = 2; + const int& result = cetl::pf17::clamp(v, lo, hi); + (void) result; +} + +TEST(DeathTestPF17Algorithm, ClampWithInvertedBounds) +{ + EXPECT_DEATH(TestClampWithInvertedBounds(), "CDE_algorithm_001"); +} + +#endif // CETL_ENABLE_DEBUG_ASSERT + +} // namespace diff --git a/cetlvast/suites/unittest/test_pf20_utility.cpp b/cetlvast/suites/unittest/test_pf20_utility.cpp new file mode 100644 index 00000000..d72f6e54 --- /dev/null +++ b/cetlvast/suites/unittest/test_pf20_utility.cpp @@ -0,0 +1,211 @@ +/// @file +/// Unit tests for cetl/pf20/utility.hpp +/// +/// @copyright +/// Copyright (C) OpenCyphal Development Team +/// Copyright Amazon.com Inc. or its affiliates. +/// SPDX-License-Identifier: MIT + +#include "cetlvast/helpers_gtest.hpp" +#include "cetl/pf20/utility.hpp" +#include "cetl/pf20/cetlpf.hpp" + +#include +#include +#include +#include +#include + +namespace +{ +using cetl::pf20::cmp_equal; +using cetl::pf20::cmp_greater; +using cetl::pf20::cmp_greater_equal; +using cetl::pf20::cmp_less; +using cetl::pf20::cmp_less_equal; +using cetl::pf20::cmp_not_equal; +using cetl::pf20::in_range; + +// +----------------------------------------------------------------------+ +// | Constant expressions: the cases where the built-in operators, after the usual arithmetic conversions, give the +// | mathematically wrong answer. +// +----------------------------------------------------------------------+ +static_assert(cmp_less(-1, 1U), "-1 < 1U is false with the built-in operator"); +static_assert(cmp_greater(1U, -1), ""); +static_assert(!cmp_equal(-1, std::numeric_limits::max()), + "-1 == UINT_MAX is true with the built-in operator"); +static_assert(cmp_not_equal(-1, std::numeric_limits::max()), ""); +static_assert(cmp_equal(1, 1U), ""); +static_assert(cmp_equal(std::int8_t{-1}, std::int64_t{-1}), ""); +static_assert(cmp_less_equal(std::int8_t{-1}, std::uint64_t{0}), ""); +static_assert(cmp_greater_equal(std::uint8_t{0}, std::int64_t{-1}), ""); +static_assert(!cmp_less(std::numeric_limits::max(), std::numeric_limits::max()), ""); +static_assert(cmp_greater(std::numeric_limits::max(), std::numeric_limits::max()), ""); +static_assert(cmp_less(std::numeric_limits::min(), std::uint8_t{0}), ""); + +static_assert(in_range(255), ""); +static_assert(!in_range(256), ""); +static_assert(!in_range(-1), ""); +static_assert(in_range(-128), ""); +static_assert(!in_range(-129), ""); +static_assert(!in_range(128), ""); +static_assert(!in_range(-1), ""); +static_assert(in_range(std::numeric_limits::max()), ""); +static_assert(!in_range(std::numeric_limits::max()), ""); +static_assert(in_range(std::numeric_limits::max()), ""); +static_assert(in_range(-32768), "cv-qualified R"); + +static_assert(cetl::cmp_less(-1, 1U), "cetlpf alias"); +static_assert(!cetl::in_range(256), "cetlpf alias"); + +// +----------------------------------------------------------------------+ +// | Sweep over a value set for every pair of fixed-width integer types, against an oracle that compares the +// | mathematical values by first looking at the signs. +// +----------------------------------------------------------------------+ +template +constexpr bool is_negative(T t, std::true_type) +{ + return t < 0; +} +template +constexpr bool is_negative(T, std::false_type) +{ + return false; +} +template +constexpr bool is_negative(T t) +{ + return is_negative(t, std::is_signed{}); +} + +/// -1, 0 or +1 for t <=> u using the mathematical values. +template +int oracle_compare(T t, U u) +{ + const bool t_negative = is_negative(t); + const bool u_negative = is_negative(u); + if (t_negative != u_negative) + { + return t_negative ? -1 : 1; + } + if (t_negative) + { + // Both negative, so both signed and both representable as long long. + const long long a = static_cast(t); + const long long b = static_cast(u); + return (a < b) ? -1 : ((a > b) ? 1 : 0); + } + // Both non-negative, so both representable as unsigned long long. + const unsigned long long a = static_cast(t); + const unsigned long long b = static_cast(u); + return (a < b) ? -1 : ((a > b) ? 1 : 0); +} + +template +bool fits(long long candidate) +{ + if (std::is_signed::value) + { + return (candidate >= static_cast(std::numeric_limits::min())) && + (candidate <= static_cast(std::numeric_limits::max())); + } + return (candidate >= 0) && (static_cast(candidate) <= + static_cast(std::numeric_limits::max())); +} + +/// The limits of T plus every boundary of the narrower fixed-width types that T can represent. +template +std::vector samples() +{ + static const long long candidates[] = {-4294967296LL, -2147483649LL, -2147483648LL, -65537LL, -65536LL, -32769LL, + -32768LL, -257LL, -256LL, -129LL, -128LL, -2LL, + -1LL, 0LL, 1LL, 2LL, 127LL, 128LL, + 255LL, 256LL, 32767LL, 32768LL, 65535LL, 65536LL, + 2147483647LL, 2147483648LL, 4294967295LL, 4294967296LL}; + std::vector out{std::numeric_limits::min(), + static_cast(std::numeric_limits::min() + 1), + static_cast(std::numeric_limits::max() - 1), + std::numeric_limits::max()}; + for (const long long candidate : candidates) + { + if (fits(candidate)) + { + out.push_back(static_cast(candidate)); + } + } + return out; +} + +template +std::string show(T value, std::true_type) +{ + return std::to_string(static_cast(value)); +} +template +std::string show(T value, std::false_type) +{ + return std::to_string(static_cast(value)); +} +template +std::string show(T value) +{ + return show(value, std::is_signed{}); +} + +template +void check_pair() +{ + for (const T t : samples()) + { + for (const U u : samples()) + { + const int c = oracle_compare(t, u); + const std::string context = show(t) + " vs " + show(u); + EXPECT_EQ(c == 0, cmp_equal(t, u)) << context; + EXPECT_EQ(c != 0, cmp_not_equal(t, u)) << context; + EXPECT_EQ(c < 0, cmp_less(t, u)) << context; + EXPECT_EQ(c > 0, cmp_greater(t, u)) << context; + EXPECT_EQ(c <= 0, cmp_less_equal(t, u)) << context; + EXPECT_EQ(c >= 0, cmp_greater_equal(t, u)) << context; + const bool u_fits_in_t = (oracle_compare(std::numeric_limits::min(), u) <= 0) && + (oracle_compare(u, std::numeric_limits::max()) <= 0); + EXPECT_EQ(u_fits_in_t, in_range(u)) << context; + } + } +} + +template +void check_against_all() +{ + check_pair(); + check_pair(); + check_pair(); + check_pair(); + check_pair(); + check_pair(); + check_pair(); + check_pair(); +} + +TEST(PF20UtilityTest, IntegerComparisonsAgainstOracle) +{ + check_against_all(); + check_against_all(); + check_against_all(); + check_against_all(); + check_against_all(); + check_against_all(); + check_against_all(); + check_against_all(); +} + +TEST(PF20UtilityTest, NonFixedWidthTypes) +{ + EXPECT_TRUE(cmp_less(-1, 1UL)); + EXPECT_TRUE(cmp_less(-1LL, 1ULL)); + EXPECT_TRUE(cmp_greater(std::numeric_limits::max(), -1L)); + EXPECT_FALSE(in_range(-1)); + EXPECT_TRUE(in_range(std::numeric_limits::min())); +} + +} // namespace diff --git a/cetlvast/suites/unittest/test_pf26_numeric.cpp b/cetlvast/suites/unittest/test_pf26_numeric.cpp new file mode 100644 index 00000000..effb2212 --- /dev/null +++ b/cetlvast/suites/unittest/test_pf26_numeric.cpp @@ -0,0 +1,495 @@ +/// @file +/// Unit tests for cetl/pf26/numeric.hpp and the saturation arithmetic selected by cetl/pf26/cetlpf.hpp. +/// +/// @copyright +/// Copyright (C) OpenCyphal Development Team +/// Copyright Amazon.com Inc. or its affiliates. +/// SPDX-License-Identifier: MIT + +#include "cetlvast/helpers_gtest.hpp" +#include "cetl/pf26/numeric.hpp" +#include "cetl/pf26/cetlpf.hpp" + +#include +#include +#include +#include +#include +#include +#include + +#if defined(__SIZEOF_INT128__) +// 128-bit integers are an extension; they are only used as a test oracle for the 64-bit types. +__extension__ typedef __int128 cetlvast_int128; +__extension__ typedef unsigned __int128 cetlvast_uint128; +# define CETLVAST_HAS_INT128 1 +#else +# define CETLVAST_HAS_INT128 0 +#endif + +namespace +{ +using cetl::pf26::saturating_add; +using cetl::pf26::saturating_cast; +using cetl::pf26::saturating_div; +using cetl::pf26::saturating_mul; +using cetl::pf26::saturating_sub; +using cetl::pf26::detail::saturating_add_portable; +using cetl::pf26::detail::saturating_mul_portable; +using cetl::pf26::detail::saturating_sub_portable; + +constexpr std::int64_t i64_min = std::numeric_limits::min(); +constexpr std::int64_t i64_max = std::numeric_limits::max(); +constexpr std::uint64_t u64_max = std::numeric_limits::max(); + +// +----------------------------------------------------------------------+ +// | Constant expressions ([numeric.sat] examples and the classic edge cases) +// +----------------------------------------------------------------------+ +static_assert(saturating_add(100, 100) == 127, ""); +static_assert(saturating_add(-100, -100) == -128, ""); +static_assert(saturating_add(-128, 127) == -1, ""); +static_assert(saturating_add(250, 10) == 255, ""); +static_assert(saturating_add(i64_max, 1) == i64_max, ""); +static_assert(saturating_add(i64_min, -1) == i64_min, ""); +static_assert(saturating_add(u64_max, 1) == u64_max, ""); +static_assert(saturating_add(3, 4) == 7, ""); + +static_assert(saturating_sub(3, 5) == 0, ""); +static_assert(saturating_sub(-128, 1) == -128, ""); +static_assert(saturating_sub(127, -1) == 127, ""); +static_assert(saturating_sub(-128, -128) == 0, ""); +static_assert(saturating_sub(0, i64_min) == i64_max, ""); +static_assert(saturating_sub(i64_min, i64_max) == i64_min, ""); +static_assert(saturating_sub(0, 1) == 0, ""); + +static_assert(saturating_mul(-128, -1) == 127, ""); +static_assert(saturating_mul(-1, -128) == 127, ""); +static_assert(saturating_mul(100, -100) == -128, ""); +static_assert(saturating_mul(-100, -100) == 127, ""); +static_assert(saturating_mul(-64, 2) == -128, ""); +static_assert(saturating_mul(16, 16) == 255, ""); +static_assert(saturating_mul(15, 17) == 255, ""); +static_assert(saturating_mul(INT64_C(4000000000), INT64_C(4000000000)) == i64_max, ""); +static_assert(saturating_mul(INT64_C(-4000000000), INT64_C(4000000000)) == i64_min, ""); +static_assert(saturating_mul(i64_min, -1) == i64_max, ""); +static_assert(saturating_mul(-3, i64_max) == i64_min, ""); +static_assert(saturating_mul(u64_max, 2) == u64_max, ""); +static_assert(saturating_mul(0, u64_max) == 0, ""); + +static_assert(saturating_div(-128, -1) == 127, ""); +static_assert(saturating_div(7, -2) == -3, "truncation toward zero"); +static_assert(saturating_div(std::numeric_limits::min(), -1) == + std::numeric_limits::max(), + ""); +static_assert(saturating_div(i64_min, -1) == i64_max, ""); +static_assert(saturating_div(i64_min, 1) == i64_min, ""); +static_assert(saturating_div(255, 1) == 255, ""); +static_assert(saturating_div(u64_max, u64_max) == 1, ""); + +static_assert(saturating_cast(696) == 127, ""); +static_assert(saturating_cast(696) == 255, ""); +static_assert(saturating_cast(-696) == -128, ""); +static_assert(saturating_cast(-696) == 0, ""); +static_assert(saturating_cast(std::uint64_t{5}) == 5, ""); +static_assert(saturating_cast(std::uint32_t{5}) == 5, ""); +static_assert(saturating_cast(std::uint32_t{0x80000000U}) == std::numeric_limits::max(), + ""); +static_assert(saturating_cast(std::int8_t{-1}) == 0, ""); +static_assert(saturating_cast(std::uint8_t{200}) == 127, ""); +static_assert(saturating_cast(u64_max) == i64_max, ""); +static_assert(saturating_cast(i64_min) == 0, ""); +static_assert(saturating_cast(i64_max) == static_cast(i64_max), ""); +static_assert(saturating_cast(i64_min) == -128, ""); +static_assert(saturating_cast(std::int64_t{-1}) == 0, ""); +static_assert(saturating_cast(INT64_C(4294967296)) == std::numeric_limits::max(), ""); +static_assert(saturating_cast(std::int64_t{42}) == 42, "same type"); + +// The portable implementations are constant expressions as well. +static_assert(saturating_add_portable(100, 100) == 127, ""); +static_assert(saturating_sub_portable(3, 5) == 0, ""); +static_assert(saturating_mul_portable(i64_min, -1) == i64_max, ""); + +// Nothing is promoted. +static_assert(std::is_same::value, ""); +static_assert(std::is_same::value, ""); +static_assert(std::is_same(std::int64_t{1})), std::uint16_t>::value, ""); + +// Whatever cetl/pf26/cetlpf.hpp selected (std, adapted std or cetl::pf26) agrees with the standard. +static_assert(cetl::saturating_add(100, 100) == 127, ""); +static_assert(cetl::saturating_sub(3, 5) == 0, ""); +static_assert(cetl::saturating_mul(-128, -1) == 127, ""); +static_assert(cetl::saturating_div(-128, -1) == 127, ""); +static_assert(cetl::saturating_cast(-5) == 0, ""); +static_assert(cetl::saturating_cast(696) == 127, ""); +static_assert(std::is_same::value, ""); + +// +----------------------------------------------------------------------+ +// | Oracles. long long is exact for every operation on the types up to 32 bits (signed) and for add/sub on the +// | unsigned 32-bit type; unsigned long long covers unsigned 32-bit multiplication. +// +----------------------------------------------------------------------+ +template +std::string show(T value) +{ + return std::is_signed::value ? std::to_string(static_cast(value)) + : std::to_string(static_cast(value)); +} + +template +long long clamp_ll(long long v) +{ + const long long lo = static_cast(std::numeric_limits::min()); + const long long hi = static_cast(std::numeric_limits::max()); + return std::min(std::max(v, lo), hi); +} + +template +unsigned long long clamp_ull(unsigned long long v) +{ + return std::min(v, static_cast(std::numeric_limits::max())); +} + +/// Checks every operation on one operand pair against the long long oracle (types up to 32 bits only). +template +void check_pair_ll(T x, T y) +{ + static_assert(sizeof(T) <= 4, "long long oracle"); + const long long a = static_cast(x); + const long long b = static_cast(y); + const std::string context = show(x) + ", " + show(y); + ASSERT_EQ(clamp_ll(a + b), static_cast(saturating_add(x, y))) << context; + ASSERT_EQ(clamp_ll(a + b), static_cast(saturating_add_portable(x, y))) << context; + ASSERT_EQ(clamp_ll(a - b), static_cast(saturating_sub(x, y))) << context; + ASSERT_EQ(clamp_ll(a - b), static_cast(saturating_sub_portable(x, y))) << context; + if (std::is_signed::value) + { + ASSERT_EQ(clamp_ll(a * b), static_cast(saturating_mul(x, y))) << context; + ASSERT_EQ(clamp_ll(a * b), static_cast(saturating_mul_portable(x, y))) << context; + } + else + { + const unsigned long long product = static_cast(a) * static_cast(b); + ASSERT_EQ(clamp_ull(product), static_cast(saturating_mul(x, y))) << context; + ASSERT_EQ(clamp_ull(product), static_cast(saturating_mul_portable(x, y))) << context; + } + if (b != 0) + { + ASSERT_EQ(clamp_ll(a / b), static_cast(saturating_div(x, y))) << context; + } +} + +template +std::vector edge_values() +{ + return {std::numeric_limits::min(), + static_cast(std::numeric_limits::min() + 1), + static_cast(0), + static_cast(1), + static_cast(2), + static_cast(-1), + static_cast(-2), + static_cast(std::numeric_limits::max() / 2), + static_cast(std::numeric_limits::max() - 1), + std::numeric_limits::max()}; +} + +// +----------------------------------------------------------------------+ +// | 8-bit types: every operand pair. +// +----------------------------------------------------------------------+ +template +void exhaustive_8bit() +{ + static_assert(sizeof(T) == 1, ""); + const int lo = std::numeric_limits::min(); + const int hi = std::numeric_limits::max(); + for (int a = lo; a <= hi; ++a) + { + for (int b = lo; b <= hi; ++b) + { + check_pair_ll(static_cast(a), static_cast(b)); + } + } +} + +TEST(PF26NumericTest, ExhaustiveInt8) +{ + exhaustive_8bit(); +} + +TEST(PF26NumericTest, ExhaustiveUint8) +{ + exhaustive_8bit(); +} + +// +----------------------------------------------------------------------+ +// | 16-bit types: every value of the first operand against the edge values and a sample of the second. +// +----------------------------------------------------------------------+ +template +void sampled_16bit() +{ + static_assert(sizeof(T) == 2, ""); + std::vector second = edge_values(); + for (long v = std::numeric_limits::min(); v <= std::numeric_limits::max(); v += 257) + { + second.push_back(static_cast(v)); + } + for (long a = std::numeric_limits::min(); a <= std::numeric_limits::max(); ++a) + { + for (const T y : second) + { + check_pair_ll(static_cast(a), y); + } + } +} + +TEST(PF26NumericTest, SampledInt16) +{ + sampled_16bit(); +} + +TEST(PF26NumericTest, SampledUint16) +{ + sampled_16bit(); +} + +// +----------------------------------------------------------------------+ +// | 32-bit types: edge values crossed with each other and with random operands. +// +----------------------------------------------------------------------+ +template +void random_32bit() +{ + static_assert(sizeof(T) == 4, ""); + const std::vector edges = edge_values(); + for (const T x : edges) + { + for (const T y : edges) + { + check_pair_ll(x, y); + } + } + std::mt19937 generator(39U); + std::uniform_int_distribution any(std::numeric_limits::min(), std::numeric_limits::max()); + for (int i = 0; i < 100000; ++i) + { + const T x = any(generator); + const T y = (i % 4 == 0) ? edges[static_cast(i / 4) % edges.size()] : any(generator); + check_pair_ll(x, y); + } +} + +TEST(PF26NumericTest, RandomInt32) +{ + random_32bit(); +} + +TEST(PF26NumericTest, RandomUint32) +{ + random_32bit(); +} + +// +----------------------------------------------------------------------+ +// | 64-bit types: edge values against hand-computed results, and (where the compiler has 128-bit integers) random +// | operands against a 128-bit oracle. +// +----------------------------------------------------------------------+ +TEST(PF26NumericTest, Int64Edges) +{ + EXPECT_EQ(i64_max, saturating_add(i64_max, std::int64_t{1})); + EXPECT_EQ(i64_max, saturating_add_portable(i64_max, std::int64_t{1})); + EXPECT_EQ(i64_min, saturating_add(i64_min, std::int64_t{-1})); + EXPECT_EQ(std::int64_t{-1}, saturating_add(i64_max, i64_min)); + EXPECT_EQ(i64_min, saturating_sub(i64_min, std::int64_t{1})); + EXPECT_EQ(i64_min, saturating_sub_portable(i64_min, std::int64_t{1})); + EXPECT_EQ(i64_max, saturating_sub(std::int64_t{0}, i64_min)); + EXPECT_EQ(i64_max, saturating_sub(i64_max, i64_min)); + EXPECT_EQ(i64_max, saturating_mul(i64_min, std::int64_t{-1})); + EXPECT_EQ(i64_max, saturating_mul_portable(i64_min, std::int64_t{-1})); + EXPECT_EQ(i64_min, saturating_mul(i64_min, std::int64_t{1})); + EXPECT_EQ(i64_min, saturating_mul(std::int64_t{2}, i64_min)); + EXPECT_EQ(i64_max, saturating_mul(std::int64_t{-2}, i64_min)); + EXPECT_EQ(INT64_C(-9223372036854775807), saturating_mul(std::int64_t{-1}, i64_max)); + EXPECT_EQ(i64_max, saturating_div(i64_min, std::int64_t{-1})); + EXPECT_EQ(INT64_C(-9223372036854775807), saturating_div(i64_max, std::int64_t{-1})); + EXPECT_EQ(std::int64_t{0}, saturating_div(std::int64_t{0}, i64_min)); +} + +TEST(PF26NumericTest, Uint64Edges) +{ + EXPECT_EQ(u64_max, saturating_add(u64_max, std::uint64_t{1})); + EXPECT_EQ(u64_max, saturating_add_portable(u64_max, std::uint64_t{1})); + EXPECT_EQ(u64_max, saturating_add(u64_max, u64_max)); + EXPECT_EQ(std::uint64_t{0}, saturating_sub(std::uint64_t{0}, u64_max)); + EXPECT_EQ(std::uint64_t{0}, saturating_sub_portable(std::uint64_t{0}, std::uint64_t{1})); + EXPECT_EQ(std::uint64_t{1}, saturating_sub(u64_max, static_cast(u64_max - 1U))); + EXPECT_EQ(u64_max, saturating_mul(u64_max, std::uint64_t{2})); + EXPECT_EQ(u64_max, saturating_mul_portable(u64_max, std::uint64_t{2})); + EXPECT_EQ(u64_max, saturating_mul(UINT64_C(4294967296), UINT64_C(4294967296))); + EXPECT_EQ(UINT64_C(18446744065119617025), saturating_mul(UINT64_C(4294967295), UINT64_C(4294967295))); + EXPECT_EQ(std::uint64_t{1}, saturating_div(u64_max, u64_max)); +} + +#if CETLVAST_HAS_INT128 + +template +using wide_t = std::conditional_t::value, cetlvast_int128, cetlvast_uint128>; + +template +T clamp_wide(cetlvast_int128 v) +{ + const cetlvast_int128 lo = std::numeric_limits::min(); + const cetlvast_int128 hi = std::numeric_limits::max(); + return static_cast(std::min(std::max(v, lo), hi)); +} + +template +void check_pair_wide(T x, T y) +{ + // Products of two 64-bit values fit in the 128-bit type of the same signedness; sums and differences are + // evaluated in the signed 128-bit type so that unsigned subtraction cannot wrap in the oracle. + const cetlvast_int128 a = x; + const cetlvast_int128 b = y; + const wide_t product = static_cast>(x) * static_cast>(y); + const std::string context = show(x) + ", " + show(y); + ASSERT_EQ(clamp_wide(a + b), saturating_add(x, y)) << context; + ASSERT_EQ(clamp_wide(a + b), saturating_add_portable(x, y)) << context; + ASSERT_EQ(clamp_wide(a - b), saturating_sub(x, y)) << context; + ASSERT_EQ(clamp_wide(a - b), saturating_sub_portable(x, y)) << context; + const T expected_product = + std::is_signed::value + ? clamp_wide(static_cast(product)) + : static_cast(std::min(static_cast(product), static_cast(u64_max))); + ASSERT_EQ(expected_product, saturating_mul(x, y)) << context; + ASSERT_EQ(expected_product, saturating_mul_portable(x, y)) << context; + if (b != 0) + { + ASSERT_EQ(clamp_wide(a / b), saturating_div(x, y)) << context; + } +} + +template +void random_64bit() +{ + static_assert(sizeof(T) == 8, ""); + const std::vector edges = edge_values(); + for (const T x : edges) + { + for (const T y : edges) + { + check_pair_wide(x, y); + } + } + std::mt19937_64 generator(39U); + std::uniform_int_distribution any(std::numeric_limits::min(), std::numeric_limits::max()); + for (int i = 0; i < 100000; ++i) + { + const T x = (i % 3 == 0) ? edges[static_cast(i / 3) % edges.size()] : any(generator); + const T y = (i % 5 == 0) ? edges[static_cast(i / 5) % edges.size()] : any(generator); + check_pair_wide(x, y); + // Values near the limits are where multiplication overflow decisions are made. + check_pair_wide(static_cast(x / 2), static_cast(y % 4)); + } +} + +TEST(PF26NumericTest, RandomInt64) +{ + random_64bit(); +} + +TEST(PF26NumericTest, RandomUint64) +{ + random_64bit(); +} + +#endif // CETLVAST_HAS_INT128 + +// +----------------------------------------------------------------------+ +// | saturating_cast: every value of the 8- and 16-bit source types into every fixed-width type. +// +----------------------------------------------------------------------+ +template +To expected_cast(long long v) +{ + if (std::is_signed::value) + { + return static_cast(clamp_ll(v)); + } + return (v < 0) ? static_cast(0) : static_cast(clamp_ull(static_cast(v))); +} + +template +void exhaustive_cast() +{ + static_assert(sizeof(From) <= 2, ""); + for (long v = std::numeric_limits::min(); v <= std::numeric_limits::max(); ++v) + { + const From x = static_cast(v); + ASSERT_EQ(expected_cast(v), saturating_cast(x)) << show(x) << " to " << sizeof(To) << " bytes"; + } +} + +template +void exhaustive_casts_from() +{ + exhaustive_cast(); + exhaustive_cast(); + exhaustive_cast(); + exhaustive_cast(); + exhaustive_cast(); + exhaustive_cast(); + exhaustive_cast(); + exhaustive_cast(); +} + +TEST(PF26NumericTest, ExhaustiveCastsFromNarrowTypes) +{ + exhaustive_casts_from(); + exhaustive_casts_from(); + exhaustive_casts_from(); + exhaustive_casts_from(); +} + +TEST(PF26NumericTest, CastsBetweenWideTypes) +{ + EXPECT_EQ(std::numeric_limits::max(), saturating_cast(i64_max)); + EXPECT_EQ(std::numeric_limits::min(), saturating_cast(i64_min)); + EXPECT_EQ(std::numeric_limits::max(), saturating_cast(u64_max)); + EXPECT_EQ(std::uint32_t{0}, saturating_cast(i64_min)); + EXPECT_EQ(i64_max, saturating_cast(u64_max)); + EXPECT_EQ(static_cast(i64_max), saturating_cast(i64_max)); + EXPECT_EQ(std::uint64_t{0}, saturating_cast(std::int64_t{-1})); + EXPECT_EQ(std::int64_t{INT64_C(4294967295)}, + saturating_cast(std::numeric_limits::max())); +} + +// +----------------------------------------------------------------------+ +// | The polyfill header's selection. +// +----------------------------------------------------------------------+ +TEST(PF26CetlPfTest, SelectedFunctionsSaturate) +{ + EXPECT_EQ(127, cetl::saturating_add(100, 100)); + EXPECT_EQ(0U, cetl::saturating_sub(3, 5)); + EXPECT_EQ(127, cetl::saturating_mul(-128, -1)); + EXPECT_EQ(127, cetl::saturating_div(-128, -1)); + EXPECT_EQ(std::numeric_limits::max(), cetl::saturating_cast(INT64_C(5000000000))); + EXPECT_EQ(0U, cetl::saturating_cast(std::int64_t{-1})); + EXPECT_EQ(u64_max, cetl::saturating_mul(u64_max, u64_max)); +} + +// +----------------------------------------------------------------------+ +// | Precondition +// +----------------------------------------------------------------------+ +#if defined(CETL_ENABLE_DEBUG_ASSERT) && CETL_ENABLE_DEBUG_ASSERT + +static void TestDivideByZero() +{ + flush_coverage_on_death(); + const int zero = 0; + const int result = saturating_div(1, zero); + (void) result; +} + +TEST(DeathTestPF26Numeric, DivideByZero) +{ + EXPECT_DEATH(TestDivideByZero(), "CDE_numeric_001"); +} + +#endif // CETL_ENABLE_DEBUG_ASSERT + +} // namespace diff --git a/include/cetl/cetl.hpp b/include/cetl/cetl.hpp index 02c17cac..7a5a10f5 100644 --- a/include/cetl/cetl.hpp +++ b/include/cetl/cetl.hpp @@ -33,6 +33,7 @@ /// - @subpage example_10_unbounded_variant /// - @subpage example_11_memory_resource_o1heap /// - @subpage example_12_buddy_memory_resource +/// - @subpage example_13_saturating_arithmetic /// /// @page example_01_polyfill_20 Example 1: CETL C++20 Polyfill Header /// Full example for @ref cetl/pf20/cetlpf.hpp @@ -83,6 +84,10 @@ /// In-place buddy merging with the same application operations as std::vector. /// @include example_12_buddy_memory_resource.cpp /// +/// @page example_13_saturating_arithmetic Example 13: Saturation arithmetic with the C++26 polyfill header +/// Full example for @ref cetl/pf26/cetlpf.hpp +/// @include example_13_saturating_arithmetic.cpp +/// #ifndef CETL_H_INCLUDED #define CETL_H_INCLUDED @@ -195,6 +200,16 @@ /// ``` #define CETL_CPP_STANDARD_20 202002L +/// @def CETL_CPP_STANDARD_23 +/// Provides the proper value to test against `__cplusplus` for c++23. +#define CETL_CPP_STANDARD_23 202302L + +/// @def CETL_CPP_STANDARD_26 +/// Provides the proper value to test against `__cplusplus` for c++26. Compilers released before the standard was +/// finalized report an interim value (for example `202400L`) for `-std=c++26`, so C++26 library features are detected +/// with their feature-test macros (see cetl/pf26/cetlpf.hpp) rather than with this value. +#define CETL_CPP_STANDARD_26 202603L + /// @} // Ensure base support. diff --git a/include/cetl/pf17/algorithm.hpp b/include/cetl/pf17/algorithm.hpp new file mode 100644 index 00000000..7e69ec97 --- /dev/null +++ b/include/cetl/pf17/algorithm.hpp @@ -0,0 +1,47 @@ +/// @file +/// Defines C++17 entities from the \c algorithm header. +/// +/// @copyright +/// Copyright (C) OpenCyphal Development Team +/// Copyright Amazon.com Inc. or its affiliates. +/// SPDX-License-Identifier: MIT + +#ifndef CETL_PF17_ALGORITHM_HPP_INCLUDED +#define CETL_PF17_ALGORITHM_HPP_INCLUDED + +#include + +#ifndef CETL_H_ERASE +# include "cetl/cetl.hpp" +#endif + +namespace cetl +{ +namespace pf17 +{ + +/// Polyfill for std::clamp with a user-provided comparison (C++17, [alg.clamp]). +/// Returns `lo` if `v` is less than `lo`, `hi` if `hi` is less than `v`, otherwise `v`, where "less" is `comp`. +/// The result is a reference to one of the arguments, so none of them may be a temporary that outlives the call. +/// +/// @pre `!comp(hi, lo)` (CDE_algorithm_001). +template +CETL_NODISCARD constexpr const T& clamp(const T& v, const T& lo, const T& hi, Compare comp) +{ + CETL_DEBUG_ASSERT(!comp(hi, lo), "CDE_algorithm_001: clamp requires that hi is not less than lo."); + return comp(v, lo) ? lo : (comp(hi, v) ? hi : v); +} + +/// Polyfill for std::clamp using `operator<` (C++17, [alg.clamp]). +/// @pre `!(hi < lo)` (CDE_algorithm_001). +template +CETL_NODISCARD constexpr const T& clamp(const T& v, const T& lo, const T& hi) +{ + // Qualified: an unqualified call would also find std::clamp through ADL on std::less (and on std types T). + return cetl::pf17::clamp(v, lo, hi, std::less<>{}); +} + +} // namespace pf17 +} // namespace cetl + +#endif // CETL_PF17_ALGORITHM_HPP_INCLUDED diff --git a/include/cetl/pf17/cetlpf.hpp b/include/cetl/pf17/cetlpf.hpp index d4858045..c4b407bd 100644 --- a/include/cetl/pf17/cetlpf.hpp +++ b/include/cetl/pf17/cetlpf.hpp @@ -45,6 +45,7 @@ # include # include # include +# include namespace cetl { @@ -128,6 +129,9 @@ using std::holds_alternative; using std::basic_string_view; using std::string_view; +// algorithm +using std::clamp; + } // namespace cetl #else @@ -138,6 +142,7 @@ using std::string_view; # include "cetl/pf17/optional.hpp" # include "cetl/pf17/variant.hpp" # include "cetl/pf17/string_view.hpp" +# include "cetl/pf17/algorithm.hpp" namespace cetl { @@ -226,6 +231,9 @@ using cetl::pf17::bad_variant_access; using cetl::pf17::basic_string_view; using cetl::pf17::string_view; +// algorithm +using cetl::pf17::clamp; + } // namespace cetl #endif diff --git a/include/cetl/pf20/cetlpf.hpp b/include/cetl/pf20/cetlpf.hpp index deba905d..31c8cfd6 100644 --- a/include/cetl/pf20/cetlpf.hpp +++ b/include/cetl/pf20/cetlpf.hpp @@ -51,6 +51,7 @@ #if (__cplusplus >= CETL_CPP_STANDARD_20 && !defined(CETL_DOXYGEN)) # include +# include namespace cetl { @@ -58,9 +59,19 @@ constexpr std::size_t dynamic_extent = std::dynamic_extent; template using span = std::span; + +// utility +using std::cmp_equal; +using std::cmp_not_equal; +using std::cmp_less; +using std::cmp_greater; +using std::cmp_less_equal; +using std::cmp_greater_equal; +using std::in_range; } // namespace cetl #else # include "cetl/pf20/span.hpp" +# include "cetl/pf20/utility.hpp" namespace cetl { @@ -69,6 +80,15 @@ constexpr std::size_t dynamic_extent = cetl::pf20::dynamic_extent; template using span = cetl::pf20::span; +// utility +using cetl::pf20::cmp_equal; +using cetl::pf20::cmp_not_equal; +using cetl::pf20::cmp_less; +using cetl::pf20::cmp_greater; +using cetl::pf20::cmp_less_equal; +using cetl::pf20::cmp_greater_equal; +using cetl::pf20::in_range; + } // namespace cetl #endif diff --git a/include/cetl/pf20/utility.hpp b/include/cetl/pf20/utility.hpp new file mode 100644 index 00000000..a64d1d40 --- /dev/null +++ b/include/cetl/pf20/utility.hpp @@ -0,0 +1,149 @@ +/// @file +/// Defines C++20 entities from the \c utility header. +/// +/// @copyright +/// Copyright (C) OpenCyphal Development Team +/// Copyright Amazon.com Inc. or its affiliates. +/// SPDX-License-Identifier: MIT + +#ifndef CETL_PF20_UTILITY_HPP_INCLUDED +#define CETL_PF20_UTILITY_HPP_INCLUDED + +#include +#include + +#ifndef CETL_H_ERASE +# include "cetl/cetl.hpp" +#endif + +namespace cetl +{ +namespace pf20 +{ +namespace detail +{ +/// True for the standard and extended integer types of [basic.fundamental]. False for `bool` and the character +/// types, which the standard excludes from the integer-comparison functions and from saturation arithmetic. +template +struct is_standard_or_extended_integer + : std::integral_constant::value && !std::is_same::value && + !std::is_same::value && !std::is_same::value && + !std::is_same::value && !std::is_same::value> +{}; +#if defined(__cpp_char8_t) +template <> +struct is_standard_or_extended_integer : std::false_type +{}; +#endif + +/// Selects the comparison strategy: 0 = same signedness, 1 = T signed and U unsigned, 2 = T unsigned and U signed. +template +using cmp_case_t = std::integral_constant::value == std::is_signed::value) ? 0 + : std::is_signed::value ? 1 + : 2>; + +template +constexpr bool cmp_equal_impl(T t, U u, std::integral_constant) noexcept +{ + return t == u; +} +template +constexpr bool cmp_equal_impl(T t, U u, std::integral_constant) noexcept +{ + return (t < 0) ? false : (static_cast>(t) == u); +} +template +constexpr bool cmp_equal_impl(T t, U u, std::integral_constant) noexcept +{ + return (u < 0) ? false : (t == static_cast>(u)); +} + +template +constexpr bool cmp_less_impl(T t, U u, std::integral_constant) noexcept +{ + return t < u; +} +template +constexpr bool cmp_less_impl(T t, U u, std::integral_constant) noexcept +{ + return (t < 0) ? true : (static_cast>(t) < u); +} +template +constexpr bool cmp_less_impl(T t, U u, std::integral_constant) noexcept +{ + return (u < 0) ? false : (t < static_cast>(u)); +} + +template +struct are_comparable_integers + : std::integral_constant::value && is_standard_or_extended_integer::value> +{}; +} // namespace detail + +/// Polyfill for std::cmp_equal (C++20, [utility.intcmp]): `t == u` with the mathematical values, so that +/// negative values never compare equal to unsigned values. +/// @pre T and U are standard or extended integer types (mandated by static assertion). +template +CETL_NODISCARD constexpr bool cmp_equal(T t, U u) noexcept +{ + static_assert(detail::are_comparable_integers::value, + "cmp_equal requires standard or extended integer types (not bool or character types)."); + return detail::cmp_equal_impl(t, u, detail::cmp_case_t{}); +} + +/// Polyfill for std::cmp_not_equal (C++20, [utility.intcmp]). +template +CETL_NODISCARD constexpr bool cmp_not_equal(T t, U u) noexcept +{ + return !cmp_equal(t, u); +} + +/// Polyfill for std::cmp_less (C++20, [utility.intcmp]): `t < u` with the mathematical values, so that +/// `cmp_less(-1, 1U)` is true where `-1 < 1U` is false. +/// @pre T and U are standard or extended integer types (mandated by static assertion). +template +CETL_NODISCARD constexpr bool cmp_less(T t, U u) noexcept +{ + static_assert(detail::are_comparable_integers::value, + "cmp_less requires standard or extended integer types (not bool or character types)."); + return detail::cmp_less_impl(t, u, detail::cmp_case_t{}); +} + +/// Polyfill for std::cmp_greater (C++20, [utility.intcmp]). +template +CETL_NODISCARD constexpr bool cmp_greater(T t, U u) noexcept +{ + return cmp_less(u, t); +} + +/// Polyfill for std::cmp_less_equal (C++20, [utility.intcmp]). +template +CETL_NODISCARD constexpr bool cmp_less_equal(T t, U u) noexcept +{ + return !cmp_less(u, t); +} + +/// Polyfill for std::cmp_greater_equal (C++20, [utility.intcmp]). +template +CETL_NODISCARD constexpr bool cmp_greater_equal(T t, U u) noexcept +{ + return !cmp_less(t, u); +} + +/// Polyfill for std::in_range (C++20, [utility.intcmp]): true if the value of `t` is representable in `R`. +/// @pre R and T are standard or extended integer types (mandated by static assertion). +template +CETL_NODISCARD constexpr bool in_range(T t) noexcept +{ + static_assert(detail::are_comparable_integers, T>::value, + "in_range requires standard or extended integer types (not bool or character types)."); + return cmp_greater_equal(t, std::numeric_limits::min()) && cmp_less_equal(t, std::numeric_limits::max()); +} + +} // namespace pf20 +} // namespace cetl + +#endif // CETL_PF20_UTILITY_HPP_INCLUDED diff --git a/include/cetl/pf26/cetlpf.hpp b/include/cetl/pf26/cetlpf.hpp new file mode 100644 index 00000000..f071358f --- /dev/null +++ b/include/cetl/pf26/cetlpf.hpp @@ -0,0 +1,105 @@ +/// @file +/// CETL polyfill header for C++26 types +/// +/// @copyright +/// Copyright (C) OpenCyphal Development Team +/// Copyright Amazon.com Inc. or its affiliates. +/// SPDX-License-Identifier: MIT +/// +/// @warning +/// This header violates AUTOSAR-14 M7-3-6, A17-0-1, and A16-0-1 (and possibly other rules). Don't use CETL polyfill +/// headers in AUTOSAR code. +/// +/// @warning +/// polyfill headers cannot be used if CETL_H_ERASE is defined. +/// +/// The saturation arithmetic functions are selected by the library feature-test macro rather than by `__cplusplus`, +/// because the standard library's support lags the language mode and because the functions were renamed late: +/// +/// - `__cpp_lib_saturation_arithmetic >= 202603L`: the standard's `std::saturating_add` family is used directly. +/// - `__cpp_lib_saturation_arithmetic >= 202311L`: the standard library only has the earlier P0543R3 names +/// (`std::add_sat`, `std::saturate_cast`, ...). CETL forwards to them under the final names, so user code is +/// written once. +/// - otherwise: cetl::pf26 provides the functions. +/// +/// For example, a counter that must stop at its maximum rather than wrap: +/// +/// @snippet{trimleft} example_13_saturating_arithmetic.cpp example_13_saturating_add +/// +/// and a narrowing conversion that clamps instead of truncating: +/// +/// @snippet{trimleft} example_13_saturating_arithmetic.cpp example_13_saturating_cast +/// +/// (@ref example_13_saturating_arithmetic "See full example here...") +/// + +#ifndef CETL_PF26_H_INCLUDED +#define CETL_PF26_H_INCLUDED + +/// @namespace cetl::pf26 This namespace contains C++26 polyfill (pf) types. +/// The types within this namespace adhere to the C++26 specification and should be drop-in replaceable +/// with said standard types where they are available. CETL polyfill types may implement a sub-set of +/// the required functionality but they will not implement non-compliant functionality. +#include "cetl/cetl.hpp" +#include "cetl/pf20/cetlpf.hpp" + +#include // Defines __cpp_lib_saturation_arithmetic where the standard library provides the feature. + +#if defined(__cpp_lib_saturation_arithmetic) && (__cpp_lib_saturation_arithmetic >= 202603L) && !defined(CETL_DOXYGEN) + +namespace cetl +{ +using std::saturating_add; +using std::saturating_sub; +using std::saturating_mul; +using std::saturating_div; +using std::saturating_cast; +} // namespace cetl + +#elif defined(__cpp_lib_saturation_arithmetic) && (__cpp_lib_saturation_arithmetic >= 202311L) && !defined(CETL_DOXYGEN) + +namespace cetl +{ +// The standard library implements P0543R3 but not the P4052R0 renaming. The standard's constraints apply. +template +CETL_NODISCARD constexpr T saturating_add(T x, T y) noexcept +{ + return std::add_sat(x, y); +} +template +CETL_NODISCARD constexpr T saturating_sub(T x, T y) noexcept +{ + return std::sub_sat(x, y); +} +template +CETL_NODISCARD constexpr T saturating_mul(T x, T y) noexcept +{ + return std::mul_sat(x, y); +} +template +CETL_NODISCARD constexpr T saturating_div(T x, T y) noexcept +{ + return std::div_sat(x, y); +} +template +CETL_NODISCARD constexpr T saturating_cast(U x) noexcept +{ + return std::saturate_cast(x); +} +} // namespace cetl + +#else +# include "cetl/pf26/numeric.hpp" + +namespace cetl +{ +using cetl::pf26::saturating_add; +using cetl::pf26::saturating_sub; +using cetl::pf26::saturating_mul; +using cetl::pf26::saturating_div; +using cetl::pf26::saturating_cast; +} // namespace cetl + +#endif + +#endif // CETL_PF26_H_INCLUDED diff --git a/include/cetl/pf26/numeric.hpp b/include/cetl/pf26/numeric.hpp new file mode 100644 index 00000000..17a82bc6 --- /dev/null +++ b/include/cetl/pf26/numeric.hpp @@ -0,0 +1,249 @@ +/// @file +/// Defines the C++26 saturation arithmetic functions of the \c numeric header ([numeric.sat]) for C++14 and newer. +/// +/// @copyright +/// Copyright (C) OpenCyphal Development Team +/// Copyright Amazon.com Inc. or its affiliates. +/// SPDX-License-Identifier: MIT +/// +/// The functions follow ISO/IEC 14882:2026 as renamed by P4052R0: `saturating_add`, `saturating_sub`, +/// `saturating_mul`, `saturating_div` and `saturating_cast`. As in the standard, the operands share one type that +/// is never promoted, mixed operand types fail deduction, and `bool` and the character types are excluded. +/// +/// Where the compiler provides `__builtin_add_overflow` and friends (GCC 5 and newer, Clang) they are used; +/// otherwise a portable implementation that never forms an overflowing expression is used. Both are constant +/// expressions in C++14. + +#ifndef CETL_PF26_NUMERIC_HPP_INCLUDED +#define CETL_PF26_NUMERIC_HPP_INCLUDED + +#include +#include + +#ifndef CETL_H_ERASE +# include "cetl/cetl.hpp" +#endif +#include "cetl/pf20/utility.hpp" + +/// @def CETL_PF26_DETAIL_OVERFLOW_BUILTINS +/// 1 when the compiler's overflow-checking builtins implement the saturating operations, 0 when the portable +/// implementation does. Not a configuration knob; exposed only so that tests can report which path is active. +#if defined(__has_builtin) +# if __has_builtin(__builtin_add_overflow) && __has_builtin(__builtin_sub_overflow) && \ + __has_builtin(__builtin_mul_overflow) +# define CETL_PF26_DETAIL_OVERFLOW_BUILTINS 1 +# endif +#elif defined(__GNUC__) && (__GNUC__ >= 5) +# define CETL_PF26_DETAIL_OVERFLOW_BUILTINS 1 +#endif +#ifndef CETL_PF26_DETAIL_OVERFLOW_BUILTINS +# define CETL_PF26_DETAIL_OVERFLOW_BUILTINS 0 +#endif + +namespace cetl +{ +namespace pf26 +{ +namespace detail +{ +/// The "signed or unsigned integer types" of [basic.fundamental]: the standard and extended integer types. +template +using is_signed_or_unsigned_integer = pf20::detail::is_standard_or_extended_integer; + +template +using enable_if_integer_t = std::enable_if_t::value, int>; + +// +----------------------------------------------------------------------------------------------------------------+ +// | Bounds returned on overflow. For signed types the operands of an overflowing operation determine the direction. +// +----------------------------------------------------------------------------------------------------------------+ +template +constexpr T add_saturated(T x, std::true_type /*signed*/) noexcept +{ + return (x < 0) ? std::numeric_limits::min() : std::numeric_limits::max(); +} +template +constexpr T add_saturated(T, std::false_type /*unsigned*/) noexcept +{ + return std::numeric_limits::max(); +} +template +constexpr T sub_saturated(T x, std::true_type /*signed*/) noexcept +{ + return (x < 0) ? std::numeric_limits::min() : std::numeric_limits::max(); +} +template +constexpr T sub_saturated(T, std::false_type /*unsigned*/) noexcept +{ + return std::numeric_limits::min(); +} +template +constexpr T mul_saturated(T x, T y, std::true_type /*signed*/) noexcept +{ + return ((x < 0) != (y < 0)) ? std::numeric_limits::min() : std::numeric_limits::max(); +} +template +constexpr T mul_saturated(T, T, std::false_type /*unsigned*/) noexcept +{ + return std::numeric_limits::max(); +} + +// +----------------------------------------------------------------------------------------------------------------+ +// | Portable overflow predicates. No expression below can itself overflow or divide by zero: the limit is moved to +// | the side of the comparison that keeps it in range, and divisors are paired with dividends of the sign that +// | cannot produce min / -1. +// +----------------------------------------------------------------------------------------------------------------+ +template +constexpr bool add_overflows(T x, T y, std::true_type /*signed*/) noexcept +{ + return (y > 0) ? (x > static_cast(std::numeric_limits::max() - y)) + : (x < static_cast(std::numeric_limits::min() - y)); +} +template +constexpr bool add_overflows(T x, T y, std::false_type /*unsigned*/) noexcept +{ + return x > static_cast(std::numeric_limits::max() - y); +} +template +constexpr bool sub_overflows(T x, T y, std::true_type /*signed*/) noexcept +{ + return (y < 0) ? (x > static_cast(std::numeric_limits::max() + y)) + : (x < static_cast(std::numeric_limits::min() + y)); +} +template +constexpr bool sub_overflows(T x, T y, std::false_type /*unsigned*/) noexcept +{ + return x < y; +} +template +constexpr bool mul_overflows(T x, T y, std::true_type /*signed*/) noexcept +{ + return ((x == 0) || (y == 0)) ? false + : (x > 0) ? ((y > 0) ? (x > std::numeric_limits::max() / y) : (y < std::numeric_limits::min() / x)) + : ((y > 0) ? (x < std::numeric_limits::min() / y) : (y < std::numeric_limits::max() / x)); +} +template +constexpr bool mul_overflows(T x, T y, std::false_type /*unsigned*/) noexcept +{ + return (y != 0) && (x > std::numeric_limits::max() / y); +} +template +constexpr bool is_min_over_minus_one(T x, T y, std::true_type /*signed*/) noexcept +{ + return (x == std::numeric_limits::min()) && (y == static_cast(-1)); +} +template +constexpr bool is_min_over_minus_one(T, T, std::false_type /*unsigned*/) noexcept +{ + return false; +} + +// +----------------------------------------------------------------------------------------------------------------+ +// | Portable implementations. These are always compiled (and unit-tested) even where the builtins are used. +// +----------------------------------------------------------------------------------------------------------------+ +template +constexpr T saturating_add_portable(T x, T y) noexcept +{ + return add_overflows(x, y, std::is_signed{}) ? add_saturated(x, std::is_signed{}) : static_cast(x + y); +} +template +constexpr T saturating_sub_portable(T x, T y) noexcept +{ + return sub_overflows(x, y, std::is_signed{}) ? sub_saturated(x, std::is_signed{}) : static_cast(x - y); +} +template +constexpr T saturating_mul_portable(T x, T y) noexcept +{ + return mul_overflows(x, y, std::is_signed{}) ? mul_saturated(x, y, std::is_signed{}) : static_cast(x * y); +} + +#if CETL_PF26_DETAIL_OVERFLOW_BUILTINS +template +constexpr T saturating_add_builtin(T x, T y) noexcept +{ + T result{}; + return __builtin_add_overflow(x, y, &result) ? add_saturated(x, std::is_signed{}) : result; +} +template +constexpr T saturating_sub_builtin(T x, T y) noexcept +{ + T result{}; + return __builtin_sub_overflow(x, y, &result) ? sub_saturated(x, std::is_signed{}) : result; +} +template +constexpr T saturating_mul_builtin(T x, T y) noexcept +{ + T result{}; + return __builtin_mul_overflow(x, y, &result) ? mul_saturated(x, y, std::is_signed{}) : result; +} +#endif +} // namespace detail + +/// Polyfill for std::saturating_add (C++26, [numeric.sat.func]). +/// Returns `x + y` if it is representable in `T`, otherwise the bound of `T` nearest to the mathematical result. +/// @tparam T A signed or unsigned integer type (not `bool` or a character type); never promoted. +template = 0> +CETL_NODISCARD constexpr T saturating_add(T x, T y) noexcept +{ +#if CETL_PF26_DETAIL_OVERFLOW_BUILTINS + return detail::saturating_add_builtin(x, y); +#else + return detail::saturating_add_portable(x, y); +#endif +} + +/// Polyfill for std::saturating_sub (C++26, [numeric.sat.func]). +/// Returns `x - y` if it is representable in `T`, otherwise the bound of `T` nearest to the mathematical result. +/// @tparam T A signed or unsigned integer type (not `bool` or a character type); never promoted. +template = 0> +CETL_NODISCARD constexpr T saturating_sub(T x, T y) noexcept +{ +#if CETL_PF26_DETAIL_OVERFLOW_BUILTINS + return detail::saturating_sub_builtin(x, y); +#else + return detail::saturating_sub_portable(x, y); +#endif +} + +/// Polyfill for std::saturating_mul (C++26, [numeric.sat.func]). +/// Returns `x * y` if it is representable in `T`, otherwise the bound of `T` nearest to the mathematical result. +/// @tparam T A signed or unsigned integer type (not `bool` or a character type); never promoted. +template = 0> +CETL_NODISCARD constexpr T saturating_mul(T x, T y) noexcept +{ +#if CETL_PF26_DETAIL_OVERFLOW_BUILTINS + return detail::saturating_mul_builtin(x, y); +#else + return detail::saturating_mul_portable(x, y); +#endif +} + +/// Polyfill for std::saturating_div (C++26, [numeric.sat.func]). +/// Returns `x / y` (truncated toward zero) if it is representable in `T`; the only other case, +/// `numeric_limits::min() / -1`, returns `numeric_limits::max()`. +/// @tparam T A signed or unsigned integer type (not `bool` or a character type); never promoted. +/// @pre `y != 0` (CDE_numeric_001). As in the standard, a zero divisor is undefined behaviour and a call that +/// divides by zero is not a constant expression. +template = 0> +CETL_NODISCARD constexpr T saturating_div(T x, T y) noexcept +{ + CETL_DEBUG_ASSERT(y != 0, "CDE_numeric_001: saturating_div requires a non-zero divisor."); + return detail::is_min_over_minus_one(x, y, std::is_signed{}) ? std::numeric_limits::max() + : static_cast(x / y); +} + +/// Polyfill for std::saturating_cast (C++26, [numeric.sat.cast]). +/// Returns `x` if it is representable in `T`, otherwise the bound of `T` nearest to `x`. +/// @tparam T The signed or unsigned integer type to convert to (not `bool` or a character type). +/// @tparam U The signed or unsigned integer type of the value (deduced). +template = 0, detail::enable_if_integer_t = 0> +CETL_NODISCARD constexpr T saturating_cast(U x) noexcept +{ + return pf20::cmp_less(x, std::numeric_limits::min()) ? std::numeric_limits::min() + : pf20::cmp_greater(x, std::numeric_limits::max()) ? std::numeric_limits::max() + : static_cast(x); +} + +} // namespace pf26 +} // namespace cetl + +#endif // CETL_PF26_NUMERIC_HPP_INCLUDED From ef0082ffdd0affeb62053c561a100b48026b4b0c Mon Sep 17 00:00:00 2001 From: Scott Dixon Date: Thu, 22 Oct 2026 10:25:30 -0700 Subject: [PATCH 2/7] Honour cv-qualifiers and extended integers in the integer traits Addresses the Codex review of #255. The shared integer traits in cetl/pf20/utility.hpp now ignore cv-qualifiers, so explicit template arguments such as saturating_add or cmp_less are rejected exactly like their unqualified forms, while const int is accepted on every implementation path. GCC's __int128 is now supported in the strict -std=c++NN modes CETLVaSt builds with, where std::is_integral, std::is_signed, std::make_unsigned and std::numeric_limits do not describe it: the traits carry their own signedness, unsigned counterpart and two's-complement limits for the 128-bit types, and cetl/pf26/numeric.hpp uses those traits throughout instead of the standard ones. The forwarders used when the standard library only has the P0543R3 names get trailing return types that depend on the standard call, so they participate in overload resolution exactly when the standard function would. Tests: cv-qualified explicit template arguments; 128-bit constant expressions for the comparison functions, in_range and all five saturation functions including the portable implementations; a compile-failure test for const bool. Co-Authored-By: Claude Fable 5.1 --- cetlvast/suites/compile/CMakeLists.txt | 1 + .../test_pf26_saturating_add_const_bool.cpp | 19 +++ .../suites/unittest/test_pf20_utility.cpp | 25 ++++ .../suites/unittest/test_pf26_numeric.cpp | 35 +++++ include/cetl/pf20/utility.hpp | 140 +++++++++++++++--- include/cetl/pf26/cetlpf.hpp | 14 +- include/cetl/pf26/numeric.hpp | 60 ++++---- 7 files changed, 240 insertions(+), 54 deletions(-) create mode 100644 cetlvast/suites/compile/test_pf26_saturating_add_const_bool.cpp diff --git a/cetlvast/suites/compile/CMakeLists.txt b/cetlvast/suites/compile/CMakeLists.txt index 3b82b1cf..542643ff 100644 --- a/cetlvast/suites/compile/CMakeLists.txt +++ b/cetlvast/suites/compile/CMakeLists.txt @@ -13,6 +13,7 @@ set(COMPILE_TESTS ${CMAKE_CURRENT_SOURCE_DIR}/test_cetl_h_erase.cpp ${CMAKE_CURRENT_SOURCE_DIR}/test_pf20_cmp_less_bool.cpp ${CMAKE_CURRENT_SOURCE_DIR}/test_pf26_saturating_add_bool.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/test_pf26_saturating_add_const_bool.cpp ${CMAKE_CURRENT_SOURCE_DIR}/test_pf26_saturating_add_mixed_types.cpp ${CMAKE_CURRENT_SOURCE_DIR}/test_span_empty.cpp ${CMAKE_CURRENT_SOURCE_DIR}/test_unbounded_variant_footprint_get_const.cpp diff --git a/cetlvast/suites/compile/test_pf26_saturating_add_const_bool.cpp b/cetlvast/suites/compile/test_pf26_saturating_add_const_bool.cpp new file mode 100644 index 00000000..19c87d8f --- /dev/null +++ b/cetlvast/suites/compile/test_pf26_saturating_add_const_bool.cpp @@ -0,0 +1,19 @@ +/// @file +/// Compile test that ensures cv-qualified bool is rejected by the saturation arithmetic functions as plain bool is. +/// +/// @copyright +/// Copyright (C) OpenCyphal Development Team +/// Copyright Amazon.com Inc. or its affiliates. +/// SPDX-License-Identifier: MIT +/// +#include "cetl/pf26/numeric.hpp" + +int main() +{ +#ifndef CETLVAST_COMPILETEST_PRECHECK + // const bool is still bool: no matching function. + return cetl::pf26::saturating_add(true, true) ? 0 : 1; +#else + return cetl::pf26::saturating_add(1, 1) - 2; +#endif +} diff --git a/cetlvast/suites/unittest/test_pf20_utility.cpp b/cetlvast/suites/unittest/test_pf20_utility.cpp index d72f6e54..8c36b0b8 100644 --- a/cetlvast/suites/unittest/test_pf20_utility.cpp +++ b/cetlvast/suites/unittest/test_pf20_utility.cpp @@ -55,6 +55,31 @@ static_assert(!in_range(std::numeric_limits::max()) static_assert(in_range(std::numeric_limits::max()), ""); static_assert(in_range(-32768), "cv-qualified R"); +// cv-qualified explicit template arguments name the same integer types. +static_assert(cmp_less(1, 2L), ""); +static_assert(cmp_equal(1U, 1), ""); +static_assert(in_range(255), ""); + +#if defined(__SIZEOF_INT128__) +// Extended 128-bit integers, including GCC's strict -std=c++NN modes where std::is_integral does not report them. +__extension__ typedef __int128 test_int128; +__extension__ typedef unsigned __int128 test_uint128; +static_assert(cmp_less(std::numeric_limits::min(), test_int128{0}), ""); +static_assert(cmp_less(test_int128{-1}, std::uint8_t{0}), ""); +static_assert(cmp_equal(test_int128{-1}, std::int8_t{-1}), ""); +static_assert(!cmp_equal(test_int128{-1}, ~test_uint128{0}), ""); +static_assert(cmp_greater(static_cast(std::numeric_limits::max()) + 1, + std::numeric_limits::max()), + ""); +static_assert(cmp_greater_equal(~test_uint128{0}, test_int128{-1}), ""); +static_assert(in_range(std::numeric_limits::max()), ""); +static_assert(!in_range(static_cast(1) << 70), ""); +static_assert(!in_range(-1), ""); +static_assert(in_range(std::numeric_limits::max()), ""); +static_assert(in_range(test_int128{-128}), ""); +static_assert(!in_range(test_int128{128}), ""); +#endif + static_assert(cetl::cmp_less(-1, 1U), "cetlpf alias"); static_assert(!cetl::in_range(256), "cetlpf alias"); diff --git a/cetlvast/suites/unittest/test_pf26_numeric.cpp b/cetlvast/suites/unittest/test_pf26_numeric.cpp index effb2212..2ce0a382 100644 --- a/cetlvast/suites/unittest/test_pf26_numeric.cpp +++ b/cetlvast/suites/unittest/test_pf26_numeric.cpp @@ -104,6 +104,41 @@ static_assert(saturating_cast(std::int64_t{-1}) == 0, ""); static_assert(saturating_cast(INT64_C(4294967296)) == std::numeric_limits::max(), ""); static_assert(saturating_cast(std::int64_t{42}) == 42, "same type"); +// cv-qualified explicit template arguments name the same integer types. +static_assert(saturating_add(100, 100) == 127, ""); +static_assert(saturating_cast(-1) == 0, ""); + +#if CETLVAST_HAS_INT128 +// Extended 128-bit integers, including GCC's strict -std=c++NN modes where std::numeric_limits does not describe them. +constexpr cetlvast_int128 i128_max = static_cast(~cetlvast_uint128{0} >> 1); +constexpr cetlvast_int128 i128_min = -i128_max - 1; +constexpr cetlvast_uint128 u128_max = ~cetlvast_uint128{0}; +static_assert(saturating_add(i128_max, cetlvast_int128{1}) == i128_max, ""); +static_assert(saturating_add(i128_min, cetlvast_int128{-1}) == i128_min, ""); +static_assert(saturating_add(cetlvast_int128{1} << 100, cetlvast_int128{1} << 100) == cetlvast_int128{1} << 101, ""); +static_assert(saturating_sub(i128_min, cetlvast_int128{1}) == i128_min, ""); +static_assert(saturating_sub(cetlvast_int128{0}, i128_min) == i128_max, ""); +static_assert(saturating_mul(i128_min, cetlvast_int128{-1}) == i128_max, ""); +static_assert(saturating_mul(cetlvast_int128{1} << 64, cetlvast_int128{1} << 64) == i128_max, ""); +static_assert(saturating_mul(cetlvast_int128{1} << 63, cetlvast_int128{1} << 63) == cetlvast_int128{1} << 126, ""); +static_assert(saturating_mul(-(cetlvast_int128{1} << 64), cetlvast_int128{1} << 64) == i128_min, ""); +static_assert(saturating_div(i128_min, cetlvast_int128{-1}) == i128_max, ""); +static_assert(saturating_div(i128_min, cetlvast_int128{2}) == -(cetlvast_int128{1} << 126), ""); +static_assert(saturating_add(u128_max, cetlvast_uint128{1}) == u128_max, ""); +static_assert(saturating_sub(cetlvast_uint128{0}, cetlvast_uint128{1}) == 0, ""); +static_assert(saturating_mul(u128_max, cetlvast_uint128{2}) == u128_max, ""); +static_assert(saturating_cast(cetlvast_int128{1} << 70) == i64_max, ""); +static_assert(saturating_cast(i128_min) == i64_min, ""); +static_assert(saturating_cast(cetlvast_int128{-1}) == 0, ""); +static_assert(saturating_cast(u128_max) == 255, ""); +static_assert(saturating_cast(u64_max) == static_cast(u64_max), ""); +static_assert(saturating_cast(i64_min) == 0, ""); +static_assert(saturating_cast(u128_max) == i128_max, ""); +static_assert(saturating_add_portable(i128_max, cetlvast_int128{1}) == i128_max, ""); +static_assert(saturating_sub_portable(cetlvast_uint128{0}, cetlvast_uint128{1}) == 0, ""); +static_assert(saturating_mul_portable(i128_min, cetlvast_int128{-1}) == i128_max, ""); +#endif + // The portable implementations are constant expressions as well. static_assert(saturating_add_portable(100, 100) == 127, ""); static_assert(saturating_sub_portable(3, 5) == 0, ""); diff --git a/include/cetl/pf20/utility.hpp b/include/cetl/pf20/utility.hpp index a64d1d40..6d17db86 100644 --- a/include/cetl/pf20/utility.hpp +++ b/include/cetl/pf20/utility.hpp @@ -22,27 +22,127 @@ namespace pf20 { namespace detail { -/// True for the standard and extended integer types of [basic.fundamental]. False for `bool` and the character -/// types, which the standard excludes from the integer-comparison functions and from saturation arithmetic. +// +----------------------------------------------------------------------------------------------------------------+ +// | Integer traits shared by the integer-comparison functions and by cetl/pf26/numeric.hpp. +// | +// | The standard traits are not enough on their own: GCC's 128-bit extended integers are not reported by +// | std::is_integral, std::is_signed, std::make_unsigned or std::numeric_limits in its strict -std=c++NN modes, and +// | cv-qualifiers must be ignored so that explicit template arguments such as `cmp_less` are still +// | rejected. +// +----------------------------------------------------------------------------------------------------------------+ +#if defined(__SIZEOF_INT128__) +__extension__ typedef __int128 int128_type; +__extension__ typedef unsigned __int128 uint128_type; template -struct is_standard_or_extended_integer +using is_int128 = + std::integral_constant::value || std::is_same::value>; +template +using is_signed_int128 = std::is_same; +#else +template +using is_int128 = std::false_type; +template +using is_signed_int128 = std::false_type; +#endif + +/// `bool` and the character types: integral, but excluded from the integer-comparison functions and from saturation +/// arithmetic by the standard. +template +struct is_bool_or_character : std::integral_constant::value && !std::is_same::value && - !std::is_same::value && !std::is_same::value && - !std::is_same::value && !std::is_same::value> + std::is_same::value || std::is_same::value || + std::is_same::value || std::is_same::value || + std::is_same::value> {}; #if defined(__cpp_char8_t) template <> -struct is_standard_or_extended_integer : std::false_type +struct is_bool_or_character : std::true_type {}; #endif +/// True for the standard and extended integer types of [basic.fundamental], ignoring cv-qualifiers. +template +using is_standard_or_extended_integer = std::integral_constant>::value && + !is_bool_or_character>::value) || + is_int128>::value>; + +/// True for the signed types among the standard and extended integer types. +template +using is_signed_integer = + std::integral_constant::value && (std::is_signed>::value || + is_signed_int128>::value)>; + +template ::value> +struct make_unsigned_integer +{ + using type = std::make_unsigned_t; +}; +#if defined(__SIZEOF_INT128__) +template +struct make_unsigned_integer +{ + using type = uint128_type; +}; +#endif +/// The unsigned counterpart of a standard or extended integer type. +template +using make_unsigned_integer_t = typename make_unsigned_integer>::type; + +/// The limits of a standard or extended integer type. std::numeric_limits is used where it is specialized for T. +template ::is_specialized> +struct integer_limits +{ + static constexpr T min() noexcept + { + return std::numeric_limits::min(); + } + static constexpr T max() noexcept + { + return std::numeric_limits::max(); + } +}; +/// Extended integers that the standard library does not describe (GCC's __int128 in strict modes) are two's +/// complement, so their limits follow from their width. +template +struct integer_limits +{ + using unsigned_type = make_unsigned_integer_t; + static constexpr T max() noexcept + { + return max_impl(is_signed_integer{}); + } + static constexpr T min() noexcept + { + return min_impl(is_signed_integer{}); + } + +private: + static constexpr T max_impl(std::true_type /*signed*/) noexcept + { + return static_cast(static_cast(~unsigned_type{0}) >> 1); + } + static constexpr T max_impl(std::false_type /*unsigned*/) noexcept + { + return static_cast(~T{0}); + } + static constexpr T min_impl(std::true_type /*signed*/) noexcept + { + return static_cast(-max() - 1); + } + static constexpr T min_impl(std::false_type /*unsigned*/) noexcept + { + return T{0}; + } +}; + /// Selects the comparison strategy: 0 = same signedness, 1 = T signed and U unsigned, 2 = T unsigned and U signed. template using cmp_case_t = std::integral_constant::value == std::is_signed::value) ? 0 - : std::is_signed::value ? 1 - : 2>; + (is_signed_integer::value == is_signed_integer::value) ? 0 + : is_signed_integer::value ? 1 + : 2>; template constexpr bool cmp_equal_impl(T t, U u, std::integral_constant) noexcept @@ -52,12 +152,12 @@ constexpr bool cmp_equal_impl(T t, U u, std::integral_constant) noexcept template constexpr bool cmp_equal_impl(T t, U u, std::integral_constant) noexcept { - return (t < 0) ? false : (static_cast>(t) == u); + return (t < 0) ? false : (static_cast>(t) == u); } template constexpr bool cmp_equal_impl(T t, U u, std::integral_constant) noexcept { - return (u < 0) ? false : (t == static_cast>(u)); + return (u < 0) ? false : (t == static_cast>(u)); } template @@ -68,19 +168,18 @@ constexpr bool cmp_less_impl(T t, U u, std::integral_constant) noexcept template constexpr bool cmp_less_impl(T t, U u, std::integral_constant) noexcept { - return (t < 0) ? true : (static_cast>(t) < u); + return (t < 0) ? true : (static_cast>(t) < u); } template constexpr bool cmp_less_impl(T t, U u, std::integral_constant) noexcept { - return (u < 0) ? false : (t < static_cast>(u)); + return (u < 0) ? false : (t < static_cast>(u)); } template -struct are_comparable_integers - : std::integral_constant::value && is_standard_or_extended_integer::value> -{}; +using are_comparable_integers = + std::integral_constant::value && is_standard_or_extended_integer::value>; } // namespace detail /// Polyfill for std::cmp_equal (C++20, [utility.intcmp]): `t == u` with the mathematical values, so that @@ -138,9 +237,10 @@ CETL_NODISCARD constexpr bool cmp_greater_equal(T t, U u) noexcept template CETL_NODISCARD constexpr bool in_range(T t) noexcept { - static_assert(detail::are_comparable_integers, T>::value, + static_assert(detail::are_comparable_integers::value, "in_range requires standard or extended integer types (not bool or character types)."); - return cmp_greater_equal(t, std::numeric_limits::min()) && cmp_less_equal(t, std::numeric_limits::max()); + return cmp_greater_equal(t, detail::integer_limits>::min()) && + cmp_less_equal(t, detail::integer_limits>::max()); } } // namespace pf20 diff --git a/include/cetl/pf26/cetlpf.hpp b/include/cetl/pf26/cetlpf.hpp index f071358f..6db4f431 100644 --- a/include/cetl/pf26/cetlpf.hpp +++ b/include/cetl/pf26/cetlpf.hpp @@ -60,29 +60,31 @@ using std::saturating_cast; namespace cetl { -// The standard library implements P0543R3 but not the P4052R0 renaming. The standard's constraints apply. +// The standard library implements P0543R3 but not the P4052R0 renaming. The trailing return types make each +// forwarder's participation in overload resolution depend on the standard call, so the standard's constraints apply +// here exactly as they would to the renamed functions. template -CETL_NODISCARD constexpr T saturating_add(T x, T y) noexcept +CETL_NODISCARD constexpr auto saturating_add(T x, T y) noexcept -> decltype(std::add_sat(x, y)) { return std::add_sat(x, y); } template -CETL_NODISCARD constexpr T saturating_sub(T x, T y) noexcept +CETL_NODISCARD constexpr auto saturating_sub(T x, T y) noexcept -> decltype(std::sub_sat(x, y)) { return std::sub_sat(x, y); } template -CETL_NODISCARD constexpr T saturating_mul(T x, T y) noexcept +CETL_NODISCARD constexpr auto saturating_mul(T x, T y) noexcept -> decltype(std::mul_sat(x, y)) { return std::mul_sat(x, y); } template -CETL_NODISCARD constexpr T saturating_div(T x, T y) noexcept +CETL_NODISCARD constexpr auto saturating_div(T x, T y) noexcept -> decltype(std::div_sat(x, y)) { return std::div_sat(x, y); } template -CETL_NODISCARD constexpr T saturating_cast(U x) noexcept +CETL_NODISCARD constexpr auto saturating_cast(U x) noexcept -> decltype(std::saturate_cast(x)) { return std::saturate_cast(x); } diff --git a/include/cetl/pf26/numeric.hpp b/include/cetl/pf26/numeric.hpp index 17a82bc6..9cc38551 100644 --- a/include/cetl/pf26/numeric.hpp +++ b/include/cetl/pf26/numeric.hpp @@ -46,9 +46,15 @@ namespace pf26 { namespace detail { -/// The "signed or unsigned integer types" of [basic.fundamental]: the standard and extended integer types. +/// The "signed or unsigned integer types" of [basic.fundamental]: the standard and extended integer types +/// (cv-qualifiers ignored, `bool` and the character types excluded). Extended 128-bit integers are supported even +/// where std::is_integral and std::numeric_limits do not describe them (GCC in strict -std=c++NN modes). template using is_signed_or_unsigned_integer = pf20::detail::is_standard_or_extended_integer; +template +using is_signed = pf20::detail::is_signed_integer; +template +using limits = pf20::detail::integer_limits; template using enable_if_integer_t = std::enable_if_t::value, int>; @@ -59,32 +65,32 @@ using enable_if_integer_t = std::enable_if_t::v template constexpr T add_saturated(T x, std::true_type /*signed*/) noexcept { - return (x < 0) ? std::numeric_limits::min() : std::numeric_limits::max(); + return (x < 0) ? limits::min() : limits::max(); } template constexpr T add_saturated(T, std::false_type /*unsigned*/) noexcept { - return std::numeric_limits::max(); + return limits::max(); } template constexpr T sub_saturated(T x, std::true_type /*signed*/) noexcept { - return (x < 0) ? std::numeric_limits::min() : std::numeric_limits::max(); + return (x < 0) ? limits::min() : limits::max(); } template constexpr T sub_saturated(T, std::false_type /*unsigned*/) noexcept { - return std::numeric_limits::min(); + return limits::min(); } template constexpr T mul_saturated(T x, T y, std::true_type /*signed*/) noexcept { - return ((x < 0) != (y < 0)) ? std::numeric_limits::min() : std::numeric_limits::max(); + return ((x < 0) != (y < 0)) ? limits::min() : limits::max(); } template constexpr T mul_saturated(T, T, std::false_type /*unsigned*/) noexcept { - return std::numeric_limits::max(); + return limits::max(); } // +----------------------------------------------------------------------------------------------------------------+ @@ -95,19 +101,17 @@ constexpr T mul_saturated(T, T, std::false_type /*unsigned*/) noexcept template constexpr bool add_overflows(T x, T y, std::true_type /*signed*/) noexcept { - return (y > 0) ? (x > static_cast(std::numeric_limits::max() - y)) - : (x < static_cast(std::numeric_limits::min() - y)); + return (y > 0) ? (x > static_cast(limits::max() - y)) : (x < static_cast(limits::min() - y)); } template constexpr bool add_overflows(T x, T y, std::false_type /*unsigned*/) noexcept { - return x > static_cast(std::numeric_limits::max() - y); + return x > static_cast(limits::max() - y); } template constexpr bool sub_overflows(T x, T y, std::true_type /*signed*/) noexcept { - return (y < 0) ? (x > static_cast(std::numeric_limits::max() + y)) - : (x < static_cast(std::numeric_limits::min() + y)); + return (y < 0) ? (x > static_cast(limits::max() + y)) : (x < static_cast(limits::min() + y)); } template constexpr bool sub_overflows(T x, T y, std::false_type /*unsigned*/) noexcept @@ -118,18 +122,18 @@ template constexpr bool mul_overflows(T x, T y, std::true_type /*signed*/) noexcept { return ((x == 0) || (y == 0)) ? false - : (x > 0) ? ((y > 0) ? (x > std::numeric_limits::max() / y) : (y < std::numeric_limits::min() / x)) - : ((y > 0) ? (x < std::numeric_limits::min() / y) : (y < std::numeric_limits::max() / x)); + : (x > 0) ? ((y > 0) ? (x > limits::max() / y) : (y < limits::min() / x)) + : ((y > 0) ? (x < limits::min() / y) : (y < limits::max() / x)); } template constexpr bool mul_overflows(T x, T y, std::false_type /*unsigned*/) noexcept { - return (y != 0) && (x > std::numeric_limits::max() / y); + return (y != 0) && (x > limits::max() / y); } template constexpr bool is_min_over_minus_one(T x, T y, std::true_type /*signed*/) noexcept { - return (x == std::numeric_limits::min()) && (y == static_cast(-1)); + return (x == limits::min()) && (y == static_cast(-1)); } template constexpr bool is_min_over_minus_one(T, T, std::false_type /*unsigned*/) noexcept @@ -143,17 +147,17 @@ constexpr bool is_min_over_minus_one(T, T, std::false_type /*unsigned*/) noexcep template constexpr T saturating_add_portable(T x, T y) noexcept { - return add_overflows(x, y, std::is_signed{}) ? add_saturated(x, std::is_signed{}) : static_cast(x + y); + return add_overflows(x, y, is_signed{}) ? add_saturated(x, is_signed{}) : static_cast(x + y); } template constexpr T saturating_sub_portable(T x, T y) noexcept { - return sub_overflows(x, y, std::is_signed{}) ? sub_saturated(x, std::is_signed{}) : static_cast(x - y); + return sub_overflows(x, y, is_signed{}) ? sub_saturated(x, is_signed{}) : static_cast(x - y); } template constexpr T saturating_mul_portable(T x, T y) noexcept { - return mul_overflows(x, y, std::is_signed{}) ? mul_saturated(x, y, std::is_signed{}) : static_cast(x * y); + return mul_overflows(x, y, is_signed{}) ? mul_saturated(x, y, is_signed{}) : static_cast(x * y); } #if CETL_PF26_DETAIL_OVERFLOW_BUILTINS @@ -161,19 +165,19 @@ template constexpr T saturating_add_builtin(T x, T y) noexcept { T result{}; - return __builtin_add_overflow(x, y, &result) ? add_saturated(x, std::is_signed{}) : result; + return __builtin_add_overflow(x, y, &result) ? add_saturated(x, is_signed{}) : result; } template constexpr T saturating_sub_builtin(T x, T y) noexcept { T result{}; - return __builtin_sub_overflow(x, y, &result) ? sub_saturated(x, std::is_signed{}) : result; + return __builtin_sub_overflow(x, y, &result) ? sub_saturated(x, is_signed{}) : result; } template constexpr T saturating_mul_builtin(T x, T y) noexcept { T result{}; - return __builtin_mul_overflow(x, y, &result) ? mul_saturated(x, y, std::is_signed{}) : result; + return __builtin_mul_overflow(x, y, &result) ? mul_saturated(x, y, is_signed{}) : result; } #endif } // namespace detail @@ -219,7 +223,7 @@ CETL_NODISCARD constexpr T saturating_mul(T x, T y) noexcept /// Polyfill for std::saturating_div (C++26, [numeric.sat.func]). /// Returns `x / y` (truncated toward zero) if it is representable in `T`; the only other case, -/// `numeric_limits::min() / -1`, returns `numeric_limits::max()`. +/// `numeric_detail::limits::min() / -1`, returns `numeric_detail::limits::max()`. /// @tparam T A signed or unsigned integer type (not `bool` or a character type); never promoted. /// @pre `y != 0` (CDE_numeric_001). As in the standard, a zero divisor is undefined behaviour and a call that /// divides by zero is not a constant expression. @@ -227,8 +231,8 @@ template = 0> CETL_NODISCARD constexpr T saturating_div(T x, T y) noexcept { CETL_DEBUG_ASSERT(y != 0, "CDE_numeric_001: saturating_div requires a non-zero divisor."); - return detail::is_min_over_minus_one(x, y, std::is_signed{}) ? std::numeric_limits::max() - : static_cast(x / y); + return detail::is_min_over_minus_one(x, y, detail::is_signed{}) ? detail::limits::max() + : static_cast(x / y); } /// Polyfill for std::saturating_cast (C++26, [numeric.sat.cast]). @@ -238,9 +242,9 @@ CETL_NODISCARD constexpr T saturating_div(T x, T y) noexcept template = 0, detail::enable_if_integer_t = 0> CETL_NODISCARD constexpr T saturating_cast(U x) noexcept { - return pf20::cmp_less(x, std::numeric_limits::min()) ? std::numeric_limits::min() - : pf20::cmp_greater(x, std::numeric_limits::max()) ? std::numeric_limits::max() - : static_cast(x); + return pf20::cmp_less(x, detail::limits::min()) ? detail::limits::min() + : pf20::cmp_greater(x, detail::limits::max()) ? detail::limits::max() + : static_cast(x); } } // namespace pf26 From 3a2e4241abbfb6c5e19d72f05890228d207085af Mon Sep 17 00:00:00 2001 From: Scott Dixon Date: Thu, 22 Oct 2026 08:09:32 -0700 Subject: [PATCH 3/7] Add C++23 expected polyfill (#13) libcyphal wants cetl::expected (its types.hpp aliases Expected to cetl::variant as a stand-in) and embedded code built without exceptions needs a way to return a value or the reason there is none. C++23 standardized that as std::expected; CETL now provides it for C++14 and newer as cetl::pf23::expected, unexpected, unexpect_t/unexpect and bad_expected_access, including expected and the monadic operations (and_then, or_else, transform, transform_error). The polyfill follows the current standard, including the defect reports that libraries apply in C++23 mode: LWG 3836 (expected from another expected), LWG 4222 (the value constructor excludes unexpect_t), P3379R0 (the equality operators are constrained rather than mandated), and remove_cv_t as the default template argument of the value constructor and value_or. Assignments that switch between the value and the error give the strong exception guarantee the standard specifies. Copy/move construction and destruction are trivial when T and E are; assignment is too, which the standard leaves unspecified. Without exceptions value() calls std::terminate, as cetl::pf17::optional does. Unchecked access (operator*, operator->, error()) has debug assertions. Known differences, documented on the class: in C++14 the in-place constructors take cetl::pf17::in_place_t (std::in_place_t from C++17 on, which is what cetl::in_place is there), members that need placement new are not constexpr, and transform() needs a movable result because C++14 has no guaranteed copy elision; cv-qualified void is not supported as the value type. cetl/pf23/cetlpf.hpp selects std::expected when __cpp_lib_expected >= 202211L (the monadic operations) and the polyfill otherwise, and cetl/pf26/cetlpf.hpp now includes it so the polyfill headers chain. Tests are typed over the implementation: where the standard library has std::expected every behavioural test also runs against it, which checks the expectations as well as the polyfill. libstdc++ 13 predates LWG 3836, so that one oracle check is skipped where the defect is detected. Coverage of expected.hpp is 99% of lines and 79% of branches (GCC 13, C++14). There are compile-failure tests for the mandates, death tests for the debug assertions and for value() without exceptions, and example 14 for the documentation. Stacked on the issue #39 branch, which adds the C++23/26 presets that exercise the std::expected path in CI. Closes #13. Co-Authored-By: Claude Opus 5.5 --- cetlvast/suites/compile/CMakeLists.txt | 3 + ...st_pf23_expected_and_then_not_expected.cpp | 20 + ...test_pf23_expected_transform_reference.cpp | 25 + ...st_pf23_expected_unexpected_value_type.cpp | 19 + cetlvast/suites/docs/examples/CMakeLists.txt | 1 + .../docs/examples/example_14_expected.cpp | 105 + cetlvast/suites/unittest/CMakeLists.txt | 3 + .../suites/unittest/test_pf23_expected.cpp | 804 ++++++ .../suites/unittest/test_pf23_expected.hpp | 465 ++++ .../unittest/test_pf23_expected_asserts.cpp | 144 ++ .../test_pf23_expected_operations.cpp | 723 ++++++ include/cetl/cetl.hpp | 5 + include/cetl/pf23/cetlpf.hpp | 75 + include/cetl/pf23/expected.hpp | 2302 +++++++++++++++++ include/cetl/pf26/cetlpf.hpp | 2 +- 15 files changed, 4695 insertions(+), 1 deletion(-) create mode 100644 cetlvast/suites/compile/test_pf23_expected_and_then_not_expected.cpp create mode 100644 cetlvast/suites/compile/test_pf23_expected_transform_reference.cpp create mode 100644 cetlvast/suites/compile/test_pf23_expected_unexpected_value_type.cpp create mode 100644 cetlvast/suites/docs/examples/example_14_expected.cpp create mode 100644 cetlvast/suites/unittest/test_pf23_expected.cpp create mode 100644 cetlvast/suites/unittest/test_pf23_expected.hpp create mode 100644 cetlvast/suites/unittest/test_pf23_expected_asserts.cpp create mode 100644 cetlvast/suites/unittest/test_pf23_expected_operations.cpp create mode 100644 include/cetl/pf23/cetlpf.hpp create mode 100644 include/cetl/pf23/expected.hpp diff --git a/cetlvast/suites/compile/CMakeLists.txt b/cetlvast/suites/compile/CMakeLists.txt index 542643ff..ce7932b0 100644 --- a/cetlvast/suites/compile/CMakeLists.txt +++ b/cetlvast/suites/compile/CMakeLists.txt @@ -12,6 +12,9 @@ include(CompileTest) set(COMPILE_TESTS ${CMAKE_CURRENT_SOURCE_DIR}/test_cetl_h_erase.cpp ${CMAKE_CURRENT_SOURCE_DIR}/test_pf20_cmp_less_bool.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/test_pf23_expected_and_then_not_expected.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/test_pf23_expected_transform_reference.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/test_pf23_expected_unexpected_value_type.cpp ${CMAKE_CURRENT_SOURCE_DIR}/test_pf26_saturating_add_bool.cpp ${CMAKE_CURRENT_SOURCE_DIR}/test_pf26_saturating_add_const_bool.cpp ${CMAKE_CURRENT_SOURCE_DIR}/test_pf26_saturating_add_mixed_types.cpp diff --git a/cetlvast/suites/compile/test_pf23_expected_and_then_not_expected.cpp b/cetlvast/suites/compile/test_pf23_expected_and_then_not_expected.cpp new file mode 100644 index 00000000..24df1ddf --- /dev/null +++ b/cetlvast/suites/compile/test_pf23_expected_and_then_not_expected.cpp @@ -0,0 +1,20 @@ +/// @file +/// Compile test that ensures and_then() rejects a callable that does not return an expected, as in C++23. +/// +/// @copyright +/// Copyright (C) OpenCyphal Development Team +/// Copyright Amazon.com Inc. or its affiliates. +/// SPDX-License-Identifier: MIT +/// +#include "cetl/pf23/expected.hpp" + +int main() +{ + const cetl::pf23::expected ex{1}; +#ifndef CETLVAST_COMPILETEST_PRECHECK + // and_then() mandates that the callable returns a specialization of expected. + return ex.and_then([](const int x) { return x; }).value(); +#else + return ex.and_then([](const int x) { return cetl::pf23::expected{x - 1}; }).value(); +#endif +} diff --git a/cetlvast/suites/compile/test_pf23_expected_transform_reference.cpp b/cetlvast/suites/compile/test_pf23_expected_transform_reference.cpp new file mode 100644 index 00000000..f87934db --- /dev/null +++ b/cetlvast/suites/compile/test_pf23_expected_transform_reference.cpp @@ -0,0 +1,25 @@ +/// @file +/// Compile test that ensures transform() rejects a callable that returns a reference, as in C++23. +/// +/// @copyright +/// Copyright (C) OpenCyphal Development Team +/// Copyright Amazon.com Inc. or its affiliates. +/// SPDX-License-Identifier: MIT +/// +#include "cetl/pf23/expected.hpp" + +namespace +{ +int storage = 0; +} + +int main() +{ + const cetl::pf23::expected ex{1}; +#ifndef CETLVAST_COMPILETEST_PRECHECK + // The new value type would be int&, which is not a valid value type for expected. + return ex.transform([](const int) -> int& { return storage; }).value(); +#else + return ex.transform([](const int) -> int { return storage; }).value(); +#endif +} diff --git a/cetlvast/suites/compile/test_pf23_expected_unexpected_value_type.cpp b/cetlvast/suites/compile/test_pf23_expected_unexpected_value_type.cpp new file mode 100644 index 00000000..c5b26472 --- /dev/null +++ b/cetlvast/suites/compile/test_pf23_expected_unexpected_value_type.cpp @@ -0,0 +1,19 @@ +/// @file +/// Compile test that ensures expected rejects a specialization of unexpected as its value type, as in C++23. +/// +/// @copyright +/// Copyright (C) OpenCyphal Development Team +/// Copyright Amazon.com Inc. or its affiliates. +/// SPDX-License-Identifier: MIT +/// +#include "cetl/pf23/expected.hpp" + +int main() +{ +#ifndef CETLVAST_COMPILETEST_PRECHECK + const cetl::pf23::expected, int> ex{cetl::pf23::unexpect, 0}; +#else + const cetl::pf23::expected ex{cetl::pf23::unexpect, 0}; +#endif + return ex.error(); +} diff --git a/cetlvast/suites/docs/examples/CMakeLists.txt b/cetlvast/suites/docs/examples/CMakeLists.txt index d43755bc..82a4b8a8 100644 --- a/cetlvast/suites/docs/examples/CMakeLists.txt +++ b/cetlvast/suites/docs/examples/CMakeLists.txt @@ -34,6 +34,7 @@ set(NATIVE_EXAMPLES example_11_memory_resource_o1heap.cpp example_12_buddy_memory_resource.cpp example_13_saturating_arithmetic.cpp + example_14_expected.cpp ) set(ALL_EXAMPLES "") diff --git a/cetlvast/suites/docs/examples/example_14_expected.cpp b/cetlvast/suites/docs/examples/example_14_expected.cpp new file mode 100644 index 00000000..18429fbf --- /dev/null +++ b/cetlvast/suites/docs/examples/example_14_expected.cpp @@ -0,0 +1,105 @@ +/// @file +/// Example of using the CETL C++23 polyfill header for expected. +/// +/// @copyright +/// Copyright (C) OpenCyphal Development Team +/// Copyright Amazon.com Inc. or its affiliates. +/// SPDX-License-Identifier: MIT +/// +#include "cetl/pf23/cetlpf.hpp" + +#include +#include +#include + +#include + +namespace +{ +//! [example_14_expected_parse] +// The cetl/pf23/cetlpf.hpp header provides cetl::expected from the standard library when it has it and from +// cetl::pf23 otherwise. Either way a function can return its result or the reason it has none, without exceptions. +enum class parse_error +{ + empty, + not_a_number, + out_of_range, +}; + +cetl::expected parse_node_id(const std::string& text) +{ + if (text.empty()) + { + return cetl::unexpected(parse_error::empty); + } + std::uint32_t value = 0; + for (const char c : text) + { + if ((c < '0') || (c > '9')) + { + return cetl::unexpected(parse_error::not_a_number); + } + value = (value * 10U) + static_cast(c - '0'); + if (value > 0x7FFFU) + { + return cetl::unexpected(parse_error::out_of_range); + } + } + return static_cast(value); +} +//! [example_14_expected_parse] + +const char* describe(const parse_error error) +{ + switch (error) + { + case parse_error::empty: + return "empty"; + case parse_error::not_a_number: + return "not a number"; + case parse_error::out_of_range: + return "out of range"; + } + return "unknown"; +} + +} // namespace + +TEST(example_14_expected, parse) +{ + const auto good = parse_node_id("42"); + const auto bad = parse_node_id("forty-two"); + if (good) + { + std::cout << "node-ID: " << *good << std::endl; // 42 + } + if (!bad) + { + std::cout << "error: " << describe(bad.error()) << std::endl; // not a number + } + ASSERT_EQ(42U, good.value_or(0)); + ASSERT_EQ(parse_error::not_a_number, bad.error()); +} + +TEST(example_14_expected, chain) +{ + //! [example_14_expected_chain] + // and_then() runs the next step only if the previous one succeeded, transform() maps a value, and + // transform_error() maps an error, so the steps compose without checking each result by hand. + const auto checked = [](const std::uint16_t id) -> cetl::expected { + if (id == 0U) + { + return cetl::unexpected(parse_error::out_of_range); // zero is reserved here + } + return id; + }; + const auto port = parse_node_id("125") + .and_then(checked) + .transform([](const std::uint16_t id) { return static_cast(id) + 1000U; }) + .transform_error([](const parse_error e) { return std::string(describe(e)); }); + std::cout << "port: " << port.value_or(0) << std::endl; // 1125 + //! [example_14_expected_chain] + ASSERT_EQ(1125U, *port); + const auto rejected = parse_node_id("0").and_then(checked); + ASSERT_EQ(parse_error::out_of_range, rejected.error()); +} diff --git a/cetlvast/suites/unittest/CMakeLists.txt b/cetlvast/suites/unittest/CMakeLists.txt index 32c64a19..daf9c9c2 100644 --- a/cetlvast/suites/unittest/CMakeLists.txt +++ b/cetlvast/suites/unittest/CMakeLists.txt @@ -110,6 +110,9 @@ set(NATIVE_TESTS test_pf20_span.cpp test_pf20.cpp test_pf20_utility.cpp + test_pf23_expected.cpp + test_pf23_expected_asserts.cpp + test_pf23_expected_operations.cpp test_pf26_numeric.cpp test_rtti.cpp test_type_traits_ext.cpp diff --git a/cetlvast/suites/unittest/test_pf23_expected.cpp b/cetlvast/suites/unittest/test_pf23_expected.cpp new file mode 100644 index 00000000..225885ad --- /dev/null +++ b/cetlvast/suites/unittest/test_pf23_expected.cpp @@ -0,0 +1,804 @@ +/// @file +/// Unit tests for cetl/pf23/expected.hpp: type properties, construction, assignment, emplace() and swap(). +/// +/// @copyright +/// Copyright (C) OpenCyphal Development Team +/// Copyright Amazon.com Inc. or its affiliates. +/// SPDX-License-Identifier: MIT + +#include "test_pf23_expected.hpp" + +#include +#include +#include + +// NOLINTBEGIN(*-use-after-move) + +namespace +{ +using namespace cetlvast::pf23_expected; // NOLINT(*-build-using-namespace) + +template +class test_pf23_expected : public test_pf23_expected_base +{}; +TYPED_TEST_SUITE(test_pf23_expected, implementations, ); + +// +----------------------------------------------------------------------+ +// | TYPE PROPERTIES +// +----------------------------------------------------------------------+ + +TYPED_TEST(test_pf23_expected, member_types) +{ + using ex = expected_t; + static_assert(std::is_same::value, ""); + static_assert(std::is_same::value, ""); + static_assert(std::is_same>::value, ""); + static_assert(std::is_same, expected_t>::value, ""); + using vex = expected_t; + static_assert(std::is_same::value, ""); + static_assert(std::is_same, expected_t>::value, ""); +} + +TYPED_TEST(test_pf23_expected, special_members_trivial) +{ + using ex = expected_t; + static_assert(std::is_trivially_copy_constructible::value, ""); + static_assert(std::is_trivially_move_constructible::value, ""); + static_assert(std::is_trivially_destructible::value, ""); + static_assert(std::is_nothrow_move_constructible::value, ""); + static_assert(std::is_copy_assignable::value, ""); + static_assert(std::is_nothrow_move_assignable::value, ""); + using vex = expected_t; + static_assert(std::is_trivially_copy_constructible::value, ""); + static_assert(std::is_trivially_move_constructible::value, ""); + static_assert(std::is_trivially_destructible::value, ""); + static_assert(std::is_nothrow_default_constructible::value, ""); +} + +TYPED_TEST(test_pf23_expected, special_members_non_trivial) +{ + using ex = expected_t; + static_assert(!std::is_trivially_copy_constructible::value, ""); + static_assert(!std::is_trivially_move_constructible::value, ""); + static_assert(!std::is_trivially_destructible::value, ""); + static_assert(std::is_copy_constructible::value, ""); + static_assert(std::is_nothrow_move_constructible::value, ""); + static_assert(std::is_copy_assignable::value, ""); + static_assert(std::is_move_assignable::value, ""); + using rex = expected_t; + static_assert(!std::is_trivially_destructible::value, ""); + static_assert(std::is_copy_assignable::value, ""); + using vex = expected_t; + static_assert(!std::is_trivially_copy_constructible::value, ""); + static_assert(!std::is_trivially_destructible::value, ""); + static_assert(std::is_copy_assignable::value, ""); +} + +TYPED_TEST(test_pf23_expected, special_members_move_only) +{ + using ex = expected_t; + static_assert(!std::is_copy_constructible::value, ""); + static_assert(std::is_move_constructible::value, ""); + static_assert(!std::is_copy_assignable::value, ""); + static_assert(std::is_move_assignable::value, ""); + using rex = expected_t; + static_assert(!std::is_copy_constructible::value, ""); + static_assert(std::is_move_constructible::value, ""); + static_assert(!std::is_copy_assignable::value, ""); + static_assert(std::is_move_assignable::value, ""); + using vex = expected_t; + static_assert(!std::is_copy_constructible::value, ""); + static_assert(std::is_move_constructible::value, ""); + static_assert(!std::is_copy_assignable::value, ""); + static_assert(std::is_move_assignable::value, ""); +} + +TYPED_TEST(test_pf23_expected, special_members_copy_only) +{ + // The move constructor does not take part in overload resolution, so rvalues are copied. + using ex = expected_t; + static_assert(std::is_copy_constructible::value, ""); + static_assert(std::is_move_constructible::value, ""); + static_assert(std::is_copy_assignable::value, ""); + static_assert(std::is_move_assignable::value, ""); +} + +TYPED_TEST(test_pf23_expected, special_members_non_movable) +{ + using ex = expected_t; + static_assert(!std::is_copy_constructible::value, ""); + static_assert(!std::is_move_constructible::value, ""); + static_assert(!std::is_copy_assignable::value, ""); + static_assert(!std::is_move_assignable::value, ""); + static_assert(std::is_default_constructible::value, ""); +} + +TYPED_TEST(test_pf23_expected, assignment_needs_one_nothrow_move) +{ + // Switching between the value and the error can give the strong exception guarantee only if one of them can be + // moved without throwing. + using neither = expected_t; + static_assert(std::is_copy_constructible::value, ""); + static_assert(!std::is_copy_assignable::value, ""); + static_assert(!std::is_move_assignable::value, ""); + using value_nothrow = expected_t; + static_assert(std::is_copy_assignable::value, ""); + static_assert(std::is_move_assignable::value, ""); + static_assert(!std::is_nothrow_move_assignable::value, ""); + using error_nothrow = expected_t; + static_assert(std::is_copy_assignable::value, ""); + static_assert(std::is_move_assignable::value, ""); + // Assigning a value or an error has the same requirement. + static_assert(!std::is_assignable::value, ""); + static_assert(!std::is_assignable>::value, ""); + static_assert(std::is_assignable::value, ""); + static_assert(std::is_assignable>::value, ""); +} + +TYPED_TEST(test_pf23_expected, default_constructible) +{ + static_assert(std::is_default_constructible>::value, ""); + static_assert(!std::is_default_constructible>::value, ""); + static_assert(std::is_default_constructible>::value, ""); + static_assert(std::is_default_constructible>::value, ""); +} + +TYPED_TEST(test_pf23_expected, implicit_and_explicit_conversions) +{ + using implicit_ex = expected_t; + using explicit_ex = expected_t; + using int_ex = expected_t; + using int_unex = unexpected_t; + // From a value. + static_assert(std::is_convertible::value, ""); + static_assert(!std::is_convertible::value, ""); + static_assert(std::is_constructible::value, ""); + // From an unexpected. + static_assert(std::is_convertible::value, ""); + static_assert(std::is_convertible::value, ""); + static_assert(!std::is_convertible::value, ""); + static_assert(!std::is_convertible::value, ""); + static_assert(std::is_constructible::value, ""); + static_assert(std::is_constructible::value, ""); + // From another expected. + static_assert(std::is_convertible::value, ""); + static_assert(std::is_convertible::value, ""); + static_assert(!std::is_convertible::value, ""); + static_assert(!std::is_convertible::value, ""); + static_assert(std::is_constructible::value, ""); + static_assert(std::is_constructible::value, ""); + // Explicit if either conversion is. + using mixed_ex = expected_t; + static_assert(!std::is_convertible::value, ""); + static_assert(std::is_constructible::value, ""); + // Not from unrelated types. + static_assert(!std::is_constructible::value, ""); + static_assert(!std::is_constructible>::value, ""); + static_assert(!std::is_constructible>::value, ""); + // A void expected converts only from another void expected. + using void_ex = expected_t; + static_assert(std::is_convertible, void_ex>::value, ""); + static_assert(!std::is_constructible::value, ""); + static_assert(!std::is_convertible, + expected_t>::value, + ""); + static_assert(std::is_constructible, + expected_t>::value, + ""); +} + +TYPED_TEST(test_pf23_expected, construction_from_tags_is_explicit) +{ + using ex = expected_t; + static_assert(!std::is_convertible::value, ""); + static_assert(!std::is_convertible::value, ""); + static_assert(std::is_constructible::value, ""); + static_assert(std::is_constructible::value, ""); + static_assert(std::is_constructible::value, ""); + static_assert(!std::is_constructible::value, ""); +} + +// +----------------------------------------------------------------------+ +// | CONSTRUCTION +// +----------------------------------------------------------------------+ + +TYPED_TEST(test_pf23_expected, default_ctor_value_initializes) +{ + const expected_t ex; + ASSERT_TRUE(ex.has_value()); + EXPECT_TRUE(static_cast(ex)); + EXPECT_EQ(0, *ex); + const expected_t counted_ex; + EXPECT_EQ(1, value_counted::stats().value_ctor); + EXPECT_EQ(0, error_counted::stats().value_ctor); +} + +TYPED_TEST(test_pf23_expected, value_ctor) +{ + using ex = expected_t; + const value_counted lvalue{3}; + const ex copied(lvalue); + EXPECT_EQ(3, copied->value); + EXPECT_EQ(1, value_counted::stats().copy_ctor); + const ex moved(value_counted{4}); + EXPECT_EQ(4, moved->value); + EXPECT_EQ(1, value_counted::stats().move_ctor); + const expected_t converted = "hello"; + EXPECT_EQ("hello", *converted); + // Empty braces default-construct the value. + const expected_t, int> braced = {}; + EXPECT_TRUE(braced.has_value()); +} + +TYPED_TEST(test_pf23_expected, unexpected_ctor) +{ + using ex = expected_t; + const unexpected_t lvalue{error_counted{5}}; + const ex copied(lvalue); + ASSERT_FALSE(copied.has_value()); + EXPECT_EQ(5, copied.error().value); + EXPECT_EQ(1, error_counted::stats().copy_ctor); + const ex moved(unexpected_t{error_counted{6}}); + ASSERT_FALSE(moved.has_value()); + EXPECT_EQ(6, moved.error().value); + EXPECT_EQ(0, value_counted::stats().value_ctor); + // The error converts. + const expected_t converted = unexpected_t(7); + EXPECT_EQ(7L, converted.error()); +} + +TYPED_TEST(test_pf23_expected, in_place_ctors) +{ + using ex = expected_t; + const ex none(TypeParam::in_place()); + EXPECT_EQ(0, none->value); + const ex two(TypeParam::in_place(), 2, 3); + EXPECT_EQ(5, two->value); + const ex il(TypeParam::in_place(), {1, 2, 3}, 4); + EXPECT_EQ(7, il->value); + EXPECT_EQ(3, value_counted::stats().value_ctor); + EXPECT_EQ(0, value_counted::stats().copy_ctor + value_counted::stats().move_ctor); + const expected_t, int> vec(TypeParam::in_place(), {1, 2, 3}); + EXPECT_EQ(3U, vec->size()); +} + +TYPED_TEST(test_pf23_expected, unexpect_ctors) +{ + using ex = expected_t; + const ex none(TypeParam::unexpect()); + ASSERT_FALSE(none.has_value()); + EXPECT_EQ(0, none.error().value); + const ex two(TypeParam::unexpect(), 2, 3); + EXPECT_EQ(5, two.error().value); + const ex il(TypeParam::unexpect(), {1, 2}, 4); + EXPECT_EQ(6, il.error().value); + EXPECT_EQ(3, error_counted::stats().value_ctor); + EXPECT_EQ(0, value_counted::stats().value_ctor); + using vex = expected_t; + const vex vil(TypeParam::unexpect(), {1, 2, 3}, 1); + EXPECT_EQ(4, vil.error().value); +} + +TYPED_TEST(test_pf23_expected, copy_and_move_ctors) +{ + using ex = expected_t; + ex value_src(TypeParam::in_place(), 1); + ex error_src(TypeParam::unexpect(), 2); + const ex value_copy(value_src); + const ex error_copy(error_src); + EXPECT_EQ(1, value_copy->value); + EXPECT_EQ(2, error_copy.error().value); + EXPECT_EQ(1, value_counted::stats().copy_ctor); + EXPECT_EQ(1, error_counted::stats().copy_ctor); + const ex value_move(std::move(value_src)); + const ex error_move(std::move(error_src)); + EXPECT_EQ(1, value_move->value); + EXPECT_EQ(2, error_move.error().value); + EXPECT_EQ(1, value_counted::stats().move_ctor); + EXPECT_EQ(1, error_counted::stats().move_ctor); + // A moved-from expected keeps its state. + EXPECT_TRUE(value_src.has_value()); + EXPECT_EQ(-1, value_src->value); + EXPECT_FALSE(error_src.has_value()); + EXPECT_EQ(-1, error_src.error().value); +} + +TYPED_TEST(test_pf23_expected, converting_ctors) +{ + using src = expected_t; + using dst = expected_t; + const src value_src(3); + const src error_src(TypeParam::unexpect(), 4); + const dst value_dst = value_src; + const dst error_dst = error_src; + EXPECT_EQ(3L, *value_dst); + EXPECT_EQ(4LL, error_dst.error()); + const expected_t explicit_value(value_src); + const expected_t explicit_error(error_src); + EXPECT_EQ(3, explicit_value->value); + EXPECT_EQ(4, explicit_error.error().value); + // Rvalues are moved. + using move_src = expected_t, std::unique_ptr>; + using move_dst = expected_t, std::shared_ptr>; + move_src value_ptr(TypeParam::in_place(), new int{5}); + move_src error_ptr(TypeParam::unexpect(), new int{6}); + const move_dst value_shared = std::move(value_ptr); + const move_dst error_shared = std::move(error_ptr); + EXPECT_EQ(5, **value_shared); + EXPECT_EQ(6, *error_shared.error()); + EXPECT_EQ(nullptr, value_ptr->get()); + EXPECT_EQ(nullptr, error_ptr.error().get()); +} + +TYPED_TEST(test_pf23_expected, converting_ctors_void) +{ + using src = expected_t; + using dst = expected_t; + const src value_src; + const src error_src(TypeParam::unexpect(), 4); + const dst value_dst = value_src; + const dst error_dst = error_src; + EXPECT_TRUE(value_dst.has_value()); + EXPECT_EQ(4L, error_dst.error()); + expected_t> ptr(TypeParam::unexpect(), new int{7}); + const expected_t> shared(std::move(ptr)); + EXPECT_EQ(7, *shared.error()); +} + +#if CETLVAST_HAS_STD_EXPECTED +/// True if the standard library implements LWG 3836 (libstdc++ 13 does not; libstdc++ 15 and libc++ 23 do). +bool std_implements_lwg3836() +{ + const std::expected zero(0); + const std::expected converted(zero); + return !*converted; +} +#endif + +TYPED_TEST(test_pf23_expected, bool_value_does_not_convert_from_expected) +{ + // An expected made from another expected copies its value rather than testing whether it has one + // (LWG 3836). +#if CETLVAST_HAS_STD_EXPECTED + if (std::is_same::value && !std_implements_lwg3836()) + { + GTEST_SKIP() << "This standard library predates LWG 3836."; + } +#endif + using bool_ex = expected_t; + const expected_t zero(0); + const bool_ex converted(zero); + ASSERT_TRUE(converted.has_value()); + EXPECT_FALSE(*converted); + const expected_t error(TypeParam::unexpect(), 1); + const bool_ex converted_error(error); + ASSERT_FALSE(converted_error.has_value()); + EXPECT_EQ(1, converted_error.error()); +} + +TYPED_TEST(test_pf23_expected, nested_expected) +{ + using inner = expected_t; + using outer = expected_t; + const outer value(inner(TypeParam::unexpect(), 1)); + ASSERT_TRUE(value.has_value()); + EXPECT_FALSE(value->has_value()); + EXPECT_EQ(1, value->error()); + const outer error(TypeParam::unexpect(), 2); + EXPECT_FALSE(error.has_value()); +} + +TYPED_TEST(test_pf23_expected, const_value_type) +{ + using ex = expected_t; + const ex value(3); + EXPECT_EQ(3, *value); + const ex copy(value); + EXPECT_EQ(3, *copy); + static_assert(!std::is_copy_assignable::value, ""); +} + +// +----------------------------------------------------------------------+ +// | ASSIGNMENT +// +----------------------------------------------------------------------+ + +TYPED_TEST(test_pf23_expected, copy_assignment) +{ + using ex = expected_t; + const ex value_src(TypeParam::in_place(), 1); + const ex error_src(TypeParam::unexpect(), 2); + { + ex dst(TypeParam::in_place(), 3); + dst = value_src; // value <- value assigns the value + EXPECT_EQ(1, dst->value); + EXPECT_EQ(1, value_counted::stats().copy_assign); + } + { + ex dst(TypeParam::unexpect(), 3); + dst = error_src; // error <- error assigns the error + EXPECT_EQ(2, dst.error().value); + EXPECT_EQ(1, error_counted::stats().copy_assign); + } + { + ex dst(TypeParam::in_place(), 3); + dst = error_src; // value <- error destroys the value and constructs the error + ASSERT_FALSE(dst.has_value()); + EXPECT_EQ(2, dst.error().value); + } + { + ex dst(TypeParam::unexpect(), 3); + dst = value_src; // error <- value destroys the error and constructs the value + ASSERT_TRUE(dst.has_value()); + EXPECT_EQ(1, dst->value); + } + { + ex dst(TypeParam::in_place(), 3); + const ex& self = dst; + dst = self; + EXPECT_EQ(3, dst->value); + } +} + +TYPED_TEST(test_pf23_expected, move_assignment) +{ + using ex = expected_t; + { + ex dst(TypeParam::in_place(), 3); + ex src(TypeParam::in_place(), 1); + dst = std::move(src); + EXPECT_EQ(1, dst->value); + EXPECT_EQ(-1, src->value); + EXPECT_EQ(1, value_counted::stats().move_assign); + } + { + ex dst(TypeParam::unexpect(), 3); + ex src(TypeParam::unexpect(), 2); + dst = std::move(src); + EXPECT_EQ(2, dst.error().value); + EXPECT_EQ(-1, src.error().value); + EXPECT_EQ(1, error_counted::stats().move_assign); + } + { + ex dst(TypeParam::in_place(), 3); + ex src(TypeParam::unexpect(), 2); + dst = std::move(src); + ASSERT_FALSE(dst.has_value()); + EXPECT_EQ(2, dst.error().value); + EXPECT_FALSE(src.has_value()); + } + { + ex dst(TypeParam::unexpect(), 3); + ex src(TypeParam::in_place(), 1); + dst = std::move(src); + ASSERT_TRUE(dst.has_value()); + EXPECT_EQ(1, dst->value); + EXPECT_TRUE(src.has_value()); + } + { + expected_t dst(TypeParam::unexpect(), 1); + dst = expected_t(TypeParam::in_place(), 4); + EXPECT_EQ(4, dst->value); + } +} + +TYPED_TEST(test_pf23_expected, value_assignment) +{ + using ex = expected_t; + ex ex1(TypeParam::in_place(), 3); + ex1 = value_counted{1}; + EXPECT_EQ(1, ex1->value); + EXPECT_EQ(1, value_counted::stats().move_assign); + ex ex2(TypeParam::unexpect(), 3); + ex2 = value_counted{2}; + ASSERT_TRUE(ex2.has_value()); + EXPECT_EQ(2, ex2->value); + expected_t str(TypeParam::unexpect(), 1); + str = "converted"; + EXPECT_EQ("converted", *str); +} + +TYPED_TEST(test_pf23_expected, unexpected_assignment) +{ + using ex = expected_t; + using unex = unexpected_t; + const unex e1(error_counted{1}); + ex ex1(TypeParam::in_place(), 3); + ex1 = e1; + ASSERT_FALSE(ex1.has_value()); + EXPECT_EQ(1, ex1.error().value); + ex1 = unex(error_counted{2}); + EXPECT_EQ(2, ex1.error().value); + EXPECT_EQ(1, error_counted::stats().move_assign); + expected_t converted(1); + converted = unexpected_t(5); + EXPECT_EQ(5L, converted.error()); +} + +TYPED_TEST(test_pf23_expected, void_assignment) +{ + using vex = expected_t; + using unex = unexpected_t; + vex value; + const vex error(TypeParam::unexpect(), 1); + value = error; + ASSERT_FALSE(value.has_value()); + EXPECT_EQ(1, value.error().value); + value = vex{}; + EXPECT_TRUE(value.has_value()); + value = unex(error_counted{2}); + EXPECT_EQ(2, value.error().value); + // The void stand-in is not moved through a temporary: the error is constructed directly. + EXPECT_EQ(0, error_counted::stats().copy_assign + error_counted::stats().move_assign); + value = unex(error_counted{3}); + EXPECT_EQ(3, value.error().value); + EXPECT_EQ(1, error_counted::stats().move_assign); + vex other(TypeParam::unexpect(), 4); + value = std::move(other); + EXPECT_EQ(4, value.error().value); +} + +// +----------------------------------------------------------------------+ +// | EMPLACE +// +----------------------------------------------------------------------+ + +TYPED_TEST(test_pf23_expected, emplace) +{ + using ex = expected_t; + ex from_value(TypeParam::in_place(), 1); + auto& ref = from_value.emplace(2, 3); + EXPECT_EQ(5, ref.value); + EXPECT_EQ(&ref, &*from_value); + ex from_error(TypeParam::unexpect(), 1); + from_error.emplace({1, 2}, 3); + ASSERT_TRUE(from_error.has_value()); + EXPECT_EQ(5, from_error->value); + EXPECT_EQ(1, error_counted::stats().dtor); + static_assert(noexcept(from_value.emplace(1)), ""); + + using vex = expected_t; + vex vvalue; + vvalue.emplace(); + EXPECT_TRUE(vvalue.has_value()); + vex verror(TypeParam::unexpect(), 1); + verror.emplace(); + EXPECT_TRUE(verror.has_value()); +} + +// +----------------------------------------------------------------------+ +// | SWAP +// +----------------------------------------------------------------------+ + +TYPED_TEST(test_pf23_expected, swap) +{ + using ex = expected_t; + ex a(TypeParam::in_place(), 1); + ex b(TypeParam::in_place(), 2); + a.swap(b); + EXPECT_EQ(2, a->value); + EXPECT_EQ(1, b->value); + ex c(TypeParam::unexpect(), 3); + ex d(TypeParam::unexpect(), 4); + swap(c, d); + EXPECT_EQ(4, c.error().value); + EXPECT_EQ(3, d.error().value); + a.swap(c); + ASSERT_FALSE(a.has_value()); + ASSERT_TRUE(c.has_value()); + EXPECT_EQ(4, a.error().value); + EXPECT_EQ(2, c->value); + swap(a, c); + ASSERT_TRUE(a.has_value()); + ASSERT_FALSE(c.has_value()); + EXPECT_EQ(2, a->value); + EXPECT_EQ(4, c.error().value); + static_assert(noexcept(a.swap(b)), ""); + static_assert(cetl::pf17::is_swappable::value, ""); +} + +TYPED_TEST(test_pf23_expected, swap_value_with_error_not_nothrow) +{ + // E is not nothrow move constructible, so the value is set aside instead. + using ex = expected_t; + static_assert(!std::is_nothrow_move_constructible::value, ""); + ex a(1); + ex b(TypeParam::unexpect(), 2); + swap(a, b); + ASSERT_FALSE(a.has_value()); + ASSERT_TRUE(b.has_value()); + EXPECT_EQ(2, a.error().value); + EXPECT_EQ(1, *b); + swap(a, b); + EXPECT_EQ(1, *a); + EXPECT_EQ(2, b.error().value); +} + +TYPED_TEST(test_pf23_expected, swap_void) +{ + using vex = expected_t; + vex a; + vex b(TypeParam::unexpect(), 2); + a.swap(b); + ASSERT_FALSE(a.has_value()); + EXPECT_TRUE(b.has_value()); + EXPECT_EQ(2, a.error().value); + swap(a, b); + EXPECT_TRUE(a.has_value()); + ASSERT_FALSE(b.has_value()); + EXPECT_EQ(2, b.error().value); + vex c(TypeParam::unexpect(), 3); + b.swap(c); + EXPECT_EQ(3, b.error().value); + EXPECT_EQ(2, c.error().value); + vex d; + a.swap(d); + EXPECT_TRUE(a.has_value()); + EXPECT_TRUE(d.has_value()); +} + +TYPED_TEST(test_pf23_expected, swap_constraints) +{ + static_assert(cetl::pf17::is_swappable>::value, ""); + static_assert(!cetl::pf17::is_swappable>::value, ""); + static_assert(!cetl::pf17::is_swappable>::value, ""); + static_assert(!cetl::pf17::is_nothrow_swappable>::value, ""); +} + +// +----------------------------------------------------------------------+ +// | EXCEPTION SAFETY +// +----------------------------------------------------------------------+ +#if defined(__cpp_exceptions) + +TYPED_TEST(test_pf23_expected, copy_assignment_strong_guarantee_via_temporary) +{ + // The new value cannot be copied without throwing but can be moved, so it is built in a temporary first. + using ex = expected_t; + const ex src(TypeParam::in_place(), 1); + ex dst(TypeParam::unexpect(), 2); + { + const arm_guard armed; + EXPECT_THROW(dst = src, test_exception); + } + ASSERT_FALSE(dst.has_value()); + EXPECT_EQ(2, dst.error()); + dst = src; + EXPECT_EQ(1, dst->value); +} + +TYPED_TEST(test_pf23_expected, copy_assignment_strong_guarantee_via_backup) +{ + // The new value cannot be copied or moved without throwing, so the old error is set aside and restored. + using ex = expected_t; + const ex src(TypeParam::in_place(), 1); + ex dst(TypeParam::unexpect(), "error"); + { + const arm_guard armed; + EXPECT_THROW(dst = src, test_exception); + } + ASSERT_FALSE(dst.has_value()); + EXPECT_EQ("error", dst.error()); + // The same with the roles swapped: the old value is set aside and restored. + using rex = expected_t; + const rex error_src(TypeParam::unexpect(), 1); + rex value_dst(TypeParam::in_place(), "value"); + { + const arm_guard armed; + EXPECT_THROW(value_dst = error_src, test_exception); + } + ASSERT_TRUE(value_dst.has_value()); + EXPECT_EQ("value", *value_dst); +} + +TYPED_TEST(test_pf23_expected, value_and_unexpected_assignment_strong_guarantee) +{ + using ex = expected_t; + const throws_on_copy value{1}; + const unexpected_t error{throws_on_copy{2}}; + ex dst(TypeParam::unexpect(), 3); + { + const arm_guard armed; + EXPECT_THROW(dst = value, test_exception); + } + ASSERT_FALSE(dst.has_value()); + EXPECT_EQ(3, dst.error().value); + dst = value; + { + const arm_guard armed; + EXPECT_THROW(dst = error, test_exception); + } + ASSERT_TRUE(dst.has_value()); + EXPECT_EQ(1, dst->value); +} + +TYPED_TEST(test_pf23_expected, swap_strong_guarantee) +{ + // E is nothrow move constructible: it is set aside, and restored when moving the value throws. + using ex = expected_t; + ex a(TypeParam::in_place(), 1); + ex b(TypeParam::unexpect(), "error"); + { + const arm_guard armed; + EXPECT_THROW(a.swap(b), test_exception); + } + ASSERT_TRUE(a.has_value()); + EXPECT_EQ(1, a->value); + ASSERT_FALSE(b.has_value()); + EXPECT_EQ("error", b.error()); +} + +TYPED_TEST(test_pf23_expected, swap_strong_guarantee_value_set_aside) +{ + using ex = expected_t; + ex a(TypeParam::in_place(), "value"); + ex b(TypeParam::unexpect(), 1); + { + const arm_guard armed; + EXPECT_THROW(swap(a, b), test_exception); + } + ASSERT_TRUE(a.has_value()); + EXPECT_EQ("value", *a); + ASSERT_FALSE(b.has_value()); + EXPECT_EQ(1, b.error().value); + swap(a, b); + EXPECT_EQ(1, a.error().value); + EXPECT_EQ("value", *b); +} + +TYPED_TEST(test_pf23_expected, constructor_exception_propagates) +{ + using ex = expected_t; + const ex value(TypeParam::in_place(), 1); + const ex error(TypeParam::unexpect(), 2); + const arm_guard armed; + EXPECT_THROW((ex{value}), test_exception); + EXPECT_THROW((ex{error}), test_exception); + const throws_on_copy v{3}; + EXPECT_THROW((ex{v}), test_exception); +} + +#endif // __cpp_exceptions + +// +----------------------------------------------------------------------+ +// | POLYFILL-SPECIFIC +// +----------------------------------------------------------------------+ + +TEST(test_pf23_expected_cetl, trivial_assignment) +{ + // The standard leaves the triviality of assignment unspecified; the polyfill makes it trivial when it can be. + using ex = cetl::pf23::expected; + static_assert(std::is_trivially_copy_assignable::value, ""); + static_assert(std::is_trivially_move_assignable::value, ""); + static_assert(std::is_trivially_copyable::value, ""); + using vex = cetl::pf23::expected; + static_assert(std::is_trivially_copyable::value, ""); + static_assert(!std::is_trivially_copy_assignable>::value, ""); +} + +TEST(test_pf23_expected_cetl, constexpr_construction_and_observers) +{ + using ex = cetl::pf23::expected; + constexpr ex value{3}; + static_assert(value.has_value(), ""); + static_assert(static_cast(value), ""); + static_assert(*value == 3, ""); + static_assert(value.value() == 3, ""); + static_assert(value.value_or(4) == 3, ""); + static_assert(value.error_or(5L) == 5L, ""); + constexpr ex error{cetl::pf23::unexpect, 6L}; + static_assert(!error.has_value(), ""); + static_assert(error.error() == 6L, ""); + static_assert(error.value_or(4) == 4, ""); + static_assert(error.error_or(5L) == 6L, ""); + constexpr ex copy{value}; + static_assert(*copy == 3, ""); + constexpr cetl::pf23::expected void_value{}; + static_assert(void_value.has_value(), ""); + constexpr cetl::pf23::unexpected unex{7}; + static_assert(unex.error() == 7, ""); + constexpr cetl::pf23::expected from_unex{unex}; + static_assert(from_unex.error() == 7L, ""); + SUCCEED(); +} + +} // namespace + +// NOLINTEND(*-use-after-move) diff --git a/cetlvast/suites/unittest/test_pf23_expected.hpp b/cetlvast/suites/unittest/test_pf23_expected.hpp new file mode 100644 index 00000000..5753d2d8 --- /dev/null +++ b/cetlvast/suites/unittest/test_pf23_expected.hpp @@ -0,0 +1,465 @@ +/// @file +/// Common types for the unit tests of cetl/pf23/expected.hpp. +/// +/// The tests are typed over an "implementation" so that, where the standard library provides std::expected with the +/// monadic operations, every behavioural test also runs against it. That checks the expectations in the tests, not +/// only the polyfill. +/// +/// @copyright +/// Copyright (C) OpenCyphal Development Team +/// Copyright Amazon.com Inc. or its affiliates. +/// SPDX-License-Identifier: MIT + +#ifndef CETLVAST_TEST_PF23_EXPECTED_HPP_INCLUDED +#define CETLVAST_TEST_PF23_EXPECTED_HPP_INCLUDED + +#include "cetl/pf23/expected.hpp" +#include "cetlvast/helpers_gtest.hpp" + +#include +#include +#include +#include + +#if (__cplusplus >= CETL_CPP_STANDARD_20) +# include +#endif +#if defined(__cpp_lib_expected) && (__cpp_lib_expected >= 202211L) +# include +# define CETLVAST_HAS_STD_EXPECTED 1 +#else +# define CETLVAST_HAS_STD_EXPECTED 0 +#endif + +namespace cetlvast +{ +namespace pf23_expected +{ + +/// The polyfill. +struct cetl_impl +{ + template + using expected = cetl::pf23::expected; + template + using unexpected = cetl::pf23::unexpected; + using unexpect_t = cetl::pf23::unexpect_t; +#if __cplusplus >= CETL_CPP_STANDARD_17 + using in_place_t = std::in_place_t; +#else + using in_place_t = cetl::pf17::in_place_t; +#endif +#if defined(__cpp_exceptions) + template + using bad_expected_access = cetl::pf23::bad_expected_access; +#endif + static constexpr unexpect_t unexpect() noexcept + { + return unexpect_t{}; + } + static constexpr in_place_t in_place() noexcept + { + return in_place_t{}; + } +}; + +#if CETLVAST_HAS_STD_EXPECTED +/// The standard library, as the oracle. +struct std_impl +{ + template + using expected = std::expected; + template + using unexpected = std::unexpected; + using unexpect_t = std::unexpect_t; + using in_place_t = std::in_place_t; +# if defined(__cpp_exceptions) + template + using bad_expected_access = std::bad_expected_access; +# endif + static constexpr unexpect_t unexpect() noexcept + { + return unexpect_t{}; + } + static constexpr in_place_t in_place() noexcept + { + return in_place_t{}; + } +}; +using implementations = ::testing::Types; +#else +using implementations = ::testing::Types; +#endif + +/// Uses a result, which a cast to void does not do for GCC's warn_unused_result (CETL_NODISCARD in C++14). +template +void consume(const T&) noexcept +{ +} + +template +using expected_t = typename Impl::template expected; + +template +using unexpected_t = typename Impl::template unexpected; + +/// Counts the special member function calls of each instantiation, so that a test can check exactly what an operation +/// did to the value and the error. +struct counters +{ + int value_ctor; + int copy_ctor; + int move_ctor; + int copy_assign; + int move_assign; + int dtor; + + int alive() const + { + return value_ctor + copy_ctor + move_ctor - dtor; + } +}; + +template +struct counted +{ + static counters& stats() + { + static counters instance{}; + return instance; + } + static void reset() + { + stats() = counters{}; + } + + explicit counted(const int v = 0) noexcept + : value{v} + { + ++stats().value_ctor; + } + counted(const int a, const int b) noexcept + : value{a + b} + { + ++stats().value_ctor; + } + counted(std::initializer_list il, const int b) noexcept + : value{static_cast(il.size()) + b} + { + ++stats().value_ctor; + } + counted(const counted& other) noexcept + : value{other.value} + { + ++stats().copy_ctor; + } + counted(counted&& other) noexcept + : value{other.value} + { + other.value = -1; + ++stats().move_ctor; + } + counted& operator=(const counted& other) noexcept + { + value = other.value; + ++stats().copy_assign; + return *this; + } + counted& operator=(counted&& other) noexcept + { + value = other.value; + other.value = -1; + ++stats().move_assign; + return *this; + } + ~counted() noexcept + { + ++stats().dtor; + } + + friend bool operator==(const counted& lhs, const counted& rhs) noexcept + { + return lhs.value == rhs.value; + } + friend bool operator!=(const counted& lhs, const counted& rhs) noexcept + { + return lhs.value != rhs.value; + } + + int value; +}; + +using value_counted = counted<0>; +using error_counted = counted<1>; + +/// Fixture that resets the counters and checks that every test leaves no value or error alive. +template +class test_pf23_expected_base : public ::testing::Test +{ +protected: + void SetUp() override + { + value_counted::reset(); + error_counted::reset(); + } + void TearDown() override + { + EXPECT_EQ(0, value_counted::stats().alive()); + EXPECT_EQ(0, error_counted::stats().alive()); + } +}; + +struct move_only +{ + explicit move_only(const int v = 0) noexcept + : value{v} + { + } + move_only(const move_only&) = delete; + move_only(move_only&&) noexcept = default; + move_only& operator=(const move_only&) = delete; + move_only& operator=(move_only&&) = default; + ~move_only() = default; + int value; +}; + +/// Copyable, but its move operations are deleted, so rvalues do not bind to its copy operations. +struct copy_only +{ + copy_only() = default; + copy_only(const copy_only&) noexcept; + copy_only(copy_only&&) = delete; + copy_only& operator=(const copy_only&) noexcept; + copy_only& operator=(copy_only&&) = delete; + ~copy_only() = default; +}; + +struct non_movable +{ + non_movable() = default; + non_movable(const non_movable&) = delete; + non_movable(non_movable&&) = delete; + non_movable& operator=(const non_movable&) = delete; + non_movable& operator=(non_movable&&) = delete; + ~non_movable() = default; +}; + +struct no_default +{ + explicit no_default(int) noexcept {} +}; + +/// Copy and move may throw (as far as the type system knows); they never actually throw. +struct maybe_throwing +{ + maybe_throwing() = default; + maybe_throwing(const maybe_throwing&) noexcept(false); + maybe_throwing(maybe_throwing&&) noexcept(false); + maybe_throwing& operator=(const maybe_throwing&) noexcept(false); + maybe_throwing& operator=(maybe_throwing&&) noexcept(false); + ~maybe_throwing() = default; +}; + +/// An int whose copy and move are not noexcept, although they never throw. +struct potentially_throwing_int +{ + explicit potentially_throwing_int(const int v) noexcept + : value{v} + { + } + potentially_throwing_int(const potentially_throwing_int& other) noexcept(false) + : value{other.value} + { + } + potentially_throwing_int(potentially_throwing_int&& other) noexcept(false) + : value{other.value} + { + } + potentially_throwing_int& operator=(const potentially_throwing_int& other) noexcept(false) + { + value = other.value; + return *this; + } + potentially_throwing_int& operator=(potentially_throwing_int&& other) noexcept(false) + { + value = other.value; + return *this; + } + ~potentially_throwing_int() = default; + + int value; +}; + +/// Constructible from int only explicitly. +struct explicit_from_int +{ + explicit explicit_from_int(const int v) noexcept + : value{v} + { + } + int value; +}; + +/// Implicitly constructible from int. +struct implicit_from_int +{ + implicit_from_int(const int v) noexcept // NOLINT(*-explicit-constructor) + : value{v} + { + } + int value; +}; + +/// The value category with which a callable was invoked. +enum class category : int +{ + lvalue = 1, + const_lvalue = 2, + rvalue = 3, + const_rvalue = 4, +}; + +/// Returns R built from the value category of its argument. +template +struct category_recorder +{ + R operator()(int&) const + { + return R(static_cast(category::lvalue)); + } + R operator()(const int&) const + { + return R(static_cast(category::const_lvalue)); + } + R operator()(int&&) const + { + return R(static_cast(category::rvalue)); + } + R operator()(const int&&) const + { + return R(static_cast(category::const_rvalue)); + } +}; + +#if defined(__cpp_exceptions) +struct test_exception final : std::exception +{ + const char* what() const noexcept override + { + return "test_exception"; + } +}; + +/// Copying throws while armed. Moving never throws. +struct throws_on_copy +{ + static bool& armed() + { + static bool instance = false; + return instance; + } + + explicit throws_on_copy(const int v) noexcept + : value{v} + { + } + throws_on_copy(const throws_on_copy& other) + : value{other.value} + { + if (armed()) + { + throw test_exception{}; + } + } + throws_on_copy(throws_on_copy&&) noexcept = default; + throws_on_copy& operator=(const throws_on_copy& other) + { + if (armed()) + { + throw test_exception{}; + } + value = other.value; + return *this; + } + throws_on_copy& operator=(throws_on_copy&&) noexcept = default; + ~throws_on_copy() = default; + + int value; +}; + +/// Copying and moving throw while armed. +struct throws_on_copy_and_move +{ + static bool& armed() + { + static bool instance = false; + return instance; + } + + explicit throws_on_copy_and_move(const int v) noexcept + : value{v} + { + } + throws_on_copy_and_move(const throws_on_copy_and_move& other) + : value{other.value} + { + if (armed()) + { + throw test_exception{}; + } + } + throws_on_copy_and_move(throws_on_copy_and_move&& other) + : value{other.value} + { + if (armed()) + { + throw test_exception{}; + } + } + throws_on_copy_and_move& operator=(const throws_on_copy_and_move& other) + { + if (armed()) + { + throw test_exception{}; + } + value = other.value; + return *this; + } + throws_on_copy_and_move& operator=(throws_on_copy_and_move&& other) + { + if (armed()) + { + throw test_exception{}; + } + value = other.value; + return *this; + } + ~throws_on_copy_and_move() = default; + + int value; +}; + +/// Arms a throwing type for the lifetime of the guard. +template +class arm_guard final +{ +public: + arm_guard() noexcept + { + T::armed() = true; + } + arm_guard(const arm_guard&) = delete; + arm_guard(arm_guard&&) = delete; + arm_guard& operator=(const arm_guard&) = delete; + arm_guard& operator=(arm_guard&&) = delete; + ~arm_guard() noexcept + { + T::armed() = false; + } +}; +#endif + +} // namespace pf23_expected +} // namespace cetlvast + +#endif // CETLVAST_TEST_PF23_EXPECTED_HPP_INCLUDED diff --git a/cetlvast/suites/unittest/test_pf23_expected_asserts.cpp b/cetlvast/suites/unittest/test_pf23_expected_asserts.cpp new file mode 100644 index 00000000..d6c987b3 --- /dev/null +++ b/cetlvast/suites/unittest/test_pf23_expected_asserts.cpp @@ -0,0 +1,144 @@ +/// @file +/// Death tests for cetl/pf23/expected.hpp: debug assertions on unchecked access, and value() without exceptions. +/// +/// @copyright +/// Copyright (C) OpenCyphal Development Team +/// Copyright Amazon.com Inc. or its affiliates. +/// SPDX-License-Identifier: MIT + +#include "cetl/cetl.hpp" +#include "cetlvast/helpers_gtest.hpp" +#include "cetl/pf23/expected.hpp" + +namespace +{ +/// Uses a result, which a cast to void does not do for GCC's warn_unused_result (CETL_NODISCARD in C++14). +template +void consume(const T&) noexcept +{ +} + +// +----------------------------------------------------------------------+ +// | DEBUG ASSERT TESTS +// +----------------------------------------------------------------------+ +#if defined(CETL_ENABLE_DEBUG_ASSERT) && CETL_ENABLE_DEBUG_ASSERT + +struct widget +{ + int value; +}; + +using ex = cetl::pf23::expected; +using vex = cetl::pf23::expected; + +void DereferenceError() +{ + flush_coverage_on_death(); + ex error{cetl::pf23::unexpect, 1}; + (void) (*error).value; +} + +void ArrowError() +{ + flush_coverage_on_death(); + const ex error{cetl::pf23::unexpect, 1}; + (void) error->value; +} + +void ErrorOfValue() +{ + flush_coverage_on_death(); + const ex value{widget{1}}; + consume(value.error()); +} + +void RvalueErrorOfValue() +{ + flush_coverage_on_death(); + consume(ex{widget{1}}.error()); +} + +void DereferenceVoidError() +{ + flush_coverage_on_death(); + const vex error{cetl::pf23::unexpect, 1}; + *error; +} + +void ErrorOfVoidValue() +{ + flush_coverage_on_death(); + const vex value{}; + consume(value.error()); +} + +TEST(DeathTestExpectedAssertions, DereferenceError) +{ + EXPECT_DEATH(DereferenceError(), "CDE_expected_004"); +} + +TEST(DeathTestExpectedAssertions, ArrowError) +{ + EXPECT_DEATH(ArrowError(), "CDE_expected_001"); +} + +TEST(DeathTestExpectedAssertions, ErrorOfValue) +{ + EXPECT_DEATH(ErrorOfValue(), "CDE_expected_007"); +} + +TEST(DeathTestExpectedAssertions, RvalueErrorOfValue) +{ + EXPECT_DEATH(RvalueErrorOfValue(), "CDE_expected_010"); +} + +TEST(DeathTestExpectedAssertions, DereferenceVoidError) +{ + EXPECT_DEATH(DereferenceVoidError(), "CDE_expected_011"); +} + +TEST(DeathTestExpectedAssertions, ErrorOfVoidValue) +{ + EXPECT_DEATH(ErrorOfVoidValue(), "CDE_expected_012"); +} + +#endif // CETL_ENABLE_DEBUG_ASSERT + +// +----------------------------------------------------------------------+ +// | NO EXCEPTIONS +// +----------------------------------------------------------------------+ +#if !defined(__cpp_exceptions) + +void ValueOfError() +{ + flush_coverage_on_death(); + const cetl::pf23::expected error{cetl::pf23::unexpect, 1}; + consume(error.value()); +} + +void ValueOfVoidError() +{ + flush_coverage_on_death(); + cetl::pf23::expected error{cetl::pf23::unexpect, 1}; + std::move(error).value(); +} + +TEST(DeathTestExpectedNoExceptions, ValueOfErrorTerminates) +{ + EXPECT_DEATH(ValueOfError(), ""); +} + +TEST(DeathTestExpectedNoExceptions, ValueOfVoidErrorTerminates) +{ + EXPECT_DEATH(ValueOfVoidError(), ""); +} + +#endif // !__cpp_exceptions + +TEST(DeathTestExpected, Placeholder) +{ + // Keeps the suite non-empty in configurations where neither group above applies. + SUCCEED(); +} + +} // namespace diff --git a/cetlvast/suites/unittest/test_pf23_expected_operations.cpp b/cetlvast/suites/unittest/test_pf23_expected_operations.cpp new file mode 100644 index 00000000..bf8ffecc --- /dev/null +++ b/cetlvast/suites/unittest/test_pf23_expected_operations.cpp @@ -0,0 +1,723 @@ +/// @file +/// Unit tests for cetl/pf23/expected.hpp: observers, monadic operations, comparison, unexpected and the polyfill +/// header. +/// +/// @copyright +/// Copyright (C) OpenCyphal Development Team +/// Copyright Amazon.com Inc. or its affiliates. +/// SPDX-License-Identifier: MIT + +#include "test_pf23_expected.hpp" +#include "cetl/pf23/cetlpf.hpp" + +#include +#include + +// NOLINTBEGIN(*-use-after-move) + +namespace +{ +using namespace cetlvast::pf23_expected; // NOLINT(*-build-using-namespace) + +template +class test_pf23_expected_operations : public test_pf23_expected_base +{}; +TYPED_TEST_SUITE(test_pf23_expected_operations, implementations, ); + +int as_int(const category c) +{ + return static_cast(c); +} + +/// Calls op with a copy of source as each of lvalue, const lvalue, rvalue and const rvalue. +template +void for_each_category(const Ex& source, Op op) +{ + Ex lvalue(source); + const Ex const_lvalue(source); + Ex rvalue(source); + const Ex const_rvalue(source); + op(lvalue); + op(const_lvalue); + op(std::move(rvalue)); + op(std::move(const_rvalue)); +} + +struct widget +{ + int get() const + { + return value; + } + int add(const int x) const + { + return value + x; + } + int value; +}; + +// +----------------------------------------------------------------------+ +// | OBSERVERS +// +----------------------------------------------------------------------+ + +TYPED_TEST(test_pf23_expected_operations, value_access) +{ + using ex = expected_t; + ex mutable_ex(widget{1}); + const ex const_ex(widget{2}); + EXPECT_EQ(1, mutable_ex->value); + EXPECT_EQ(2, const_ex->value); + EXPECT_EQ(1, (*mutable_ex).value); + EXPECT_EQ(2, (*const_ex).value); + EXPECT_EQ(1, mutable_ex.value().value); + EXPECT_EQ(2, const_ex.value().value); + mutable_ex->value = 3; + EXPECT_EQ(3, mutable_ex.value().value); + EXPECT_EQ(2, std::move(const_ex).value().value); + EXPECT_EQ(3, std::move(mutable_ex).value().value); + static_assert(std::is_same::value, ""); + static_assert(std::is_same::value, ""); + static_assert(std::is_same::value, ""); + static_assert(std::is_same::value, ""); + static_assert(std::is_same::value, ""); + static_assert(std::is_same::value, ""); + static_assert(std::is_same::value, ""); + static_assert(std::is_same::value, ""); + static_assert(std::is_same()), widget*>::value, ""); + static_assert(std::is_same()), const widget*>::value, ""); + static_assert(noexcept(*mutable_ex), ""); + static_assert(noexcept(mutable_ex.has_value()), ""); +} + +TYPED_TEST(test_pf23_expected_operations, error_access) +{ + using ex = expected_t; + ex mutable_ex(TypeParam::unexpect(), widget{1}); + const ex const_ex(TypeParam::unexpect(), widget{2}); + EXPECT_EQ(1, mutable_ex.error().value); + EXPECT_EQ(2, const_ex.error().value); + static_assert(std::is_same::value, ""); + static_assert(std::is_same::value, ""); + static_assert(std::is_same::value, ""); + static_assert(std::is_same::value, ""); + static_assert(noexcept(mutable_ex.error()), ""); + using vex = expected_t; + vex mutable_vex(TypeParam::unexpect(), widget{3}); + const vex const_vex(TypeParam::unexpect(), widget{4}); + EXPECT_EQ(3, mutable_vex.error().value); + EXPECT_EQ(4, const_vex.error().value); + static_assert(std::is_same::value, ""); + static_assert(std::is_same::value, ""); +} + +TYPED_TEST(test_pf23_expected_operations, value_or) +{ + using ex = expected_t; + const ex value("value"); + const ex error(TypeParam::unexpect(), 1); + EXPECT_EQ("value", value.value_or("default")); + EXPECT_EQ("default", error.value_or("default")); + ex movable("moved"); + EXPECT_EQ("moved", std::move(movable).value_or("default")); + EXPECT_TRUE(movable->empty()); + ex movable_error(TypeParam::unexpect(), 2); + EXPECT_EQ("default", std::move(movable_error).value_or("default")); +} + +TYPED_TEST(test_pf23_expected_operations, error_or) +{ + using ex = expected_t; + const ex value(1); + const ex error(TypeParam::unexpect(), "error"); + EXPECT_EQ("default", value.error_or("default")); + EXPECT_EQ("error", error.error_or("default")); + ex movable(TypeParam::unexpect(), "moved"); + EXPECT_EQ("moved", std::move(movable).error_or("default")); + EXPECT_TRUE(movable.error().empty()); + using vex = expected_t; + const vex vvalue; + const vex verror(TypeParam::unexpect(), "error"); + EXPECT_EQ("default", vvalue.error_or("default")); + EXPECT_EQ("error", verror.error_or("default")); + vex vmovable(TypeParam::unexpect(), "moved"); + EXPECT_EQ("moved", std::move(vmovable).error_or("default")); +} + +TYPED_TEST(test_pf23_expected_operations, void_value_access) +{ + using vex = expected_t; + const vex value; + EXPECT_TRUE(value.has_value()); + EXPECT_TRUE(static_cast(value)); + static_assert(std::is_same::value, ""); + static_assert(std::is_same::value, ""); + static_assert(std::is_same::value, ""); + *value; + value.value(); + const vex in_place(TypeParam::in_place()); + EXPECT_TRUE(in_place.has_value()); + const vex error(TypeParam::unexpect(), 1); + EXPECT_FALSE(error.has_value()); + EXPECT_FALSE(static_cast(error)); +} + +#if defined(__cpp_exceptions) + +TYPED_TEST(test_pf23_expected_operations, value_throws_bad_expected_access) +{ + using ex = expected_t; + using access = typename TypeParam::template bad_expected_access; + ex error(TypeParam::unexpect(), "error"); + const ex const_error(TypeParam::unexpect(), "const error"); + try + { + consume(error.value()); + FAIL() << "value() did not throw"; + } catch (const access& e) + { + EXPECT_EQ("error", e.error()); + } + try + { + consume(const_error.value()); + FAIL() << "value() did not throw"; + } catch (const access& e) + { + EXPECT_EQ("const error", e.error()); + } + // An rvalue moves its error into the exception. + try + { + consume(std::move(error).value()); + FAIL() << "value() did not throw"; + } catch (access& e) + { + EXPECT_EQ("error", e.error()); + EXPECT_EQ("error", std::move(e).error()); + EXPECT_TRUE(error.error().empty()); + } + EXPECT_THROW(consume(std::move(const_error).value()), access); + // The exception can be caught through its bases. + using base_access = typename TypeParam::template bad_expected_access; + EXPECT_THROW(consume(const_error.value()), base_access); + EXPECT_THROW(consume(const_error.value()), std::exception); + try + { + consume(const_error.value()); + } catch (const std::exception& e) + { + EXPECT_NE(nullptr, e.what()); + } + static_assert(!std::is_default_constructible::value, ""); +} + +TYPED_TEST(test_pf23_expected_operations, void_value_throws_bad_expected_access) +{ + using vex = expected_t; + using access = typename TypeParam::template bad_expected_access; + vex error(TypeParam::unexpect(), 1); + try + { + error.value(); + FAIL() << "value() did not throw"; + } catch (const access& e) + { + EXPECT_EQ(1, e.error()); + } + EXPECT_THROW(std::move(error).value(), access); +} + +TEST(test_pf23_expected_cetl, bad_expected_access_what) +{ + const cetl::pf23::bad_expected_access e{3}; + EXPECT_STREQ("bad_expected_access", e.what()); + EXPECT_EQ(3, e.error()); + cetl::pf23::bad_expected_access copy{e}; + copy = e; + EXPECT_EQ(3, copy.error()); +} + +#endif // __cpp_exceptions + +// +----------------------------------------------------------------------+ +// | MONADIC OPERATIONS +// +----------------------------------------------------------------------+ + +TYPED_TEST(test_pf23_expected_operations, and_then) +{ + using ex = expected_t; + const auto half = [](const int x) -> ex { + if (x % 2 != 0) + { + return ex(TypeParam::unexpect(), "odd"); + } + return x / 2; + }; + EXPECT_EQ(2, *ex(8).and_then(half).and_then(half)); + EXPECT_EQ("odd", ex(6).and_then(half).and_then(half).error()); + int calls = 0; + const auto count = [&calls](const int x) -> ex { + ++calls; + return x; + }; + EXPECT_EQ("error", ex(TypeParam::unexpect(), "error").and_then(count).error()); + EXPECT_EQ(0, calls); + // The result may have a different value type. + const auto to_string = [](const int x) { + return expected_t(std::to_string(x)); + }; + EXPECT_EQ("3", *ex(3).and_then(to_string)); +} + +TYPED_TEST(test_pf23_expected_operations, and_then_value_categories) +{ + using ex = expected_t; + using recorder = category_recorder; + ex value(0); + const ex const_value(0); + EXPECT_EQ(as_int(category::lvalue), *value.and_then(recorder{})); + EXPECT_EQ(as_int(category::const_lvalue), *const_value.and_then(recorder{})); + EXPECT_EQ(as_int(category::rvalue), *std::move(value).and_then(recorder{})); + EXPECT_EQ(as_int(category::const_rvalue), *std::move(const_value).and_then(recorder{})); + // The error is forwarded with the same category. + using mex = expected_t>; + const auto keep = [](int x) { return mex(x); }; + mex error(TypeParam::unexpect(), new int{5}); + const mex result = std::move(error).and_then(keep); + EXPECT_EQ(5, *result.error()); + EXPECT_EQ(nullptr, error.error().get()); +} + +TYPED_TEST(test_pf23_expected_operations, or_else) +{ + using ex = expected_t; + const auto recover = [](const std::string& e) { return ex(static_cast(e.size())); }; + EXPECT_EQ(5, *ex(TypeParam::unexpect(), "error").or_else(recover)); + EXPECT_EQ(1, *ex(1).or_else(recover)); + // The result may have a different error type. + const auto rethrow = [](const std::string& e) { + return expected_t(TypeParam::unexpect(), static_cast(e.size())); + }; + EXPECT_EQ(5, ex(TypeParam::unexpect(), "error").or_else(rethrow).error()); + EXPECT_EQ(7, *ex(7).or_else(rethrow)); + // Value categories of the error, and of the value carried over. + using iex = expected_t; + using recorder = category_recorder; + iex error(TypeParam::unexpect(), 0); + const iex const_error(TypeParam::unexpect(), 0); + EXPECT_EQ(as_int(category::lvalue), *error.or_else(recorder{})); + EXPECT_EQ(as_int(category::const_lvalue), *const_error.or_else(recorder{})); + EXPECT_EQ(as_int(category::rvalue), *std::move(error).or_else(recorder{})); + EXPECT_EQ(as_int(category::const_rvalue), *std::move(const_error).or_else(recorder{})); + using mex = expected_t, int>; + mex value(TypeParam::in_place(), new int{6}); + const mex result = std::move(value).or_else([](int e) { return mex(TypeParam::unexpect(), e); }); + EXPECT_EQ(6, **result); + EXPECT_EQ(nullptr, value->get()); +} + +TYPED_TEST(test_pf23_expected_operations, transform) +{ + using ex = expected_t; + const auto twice = [](const int x) { return 2 * x; }; + const auto string = [](const int x) { return std::to_string(x); }; + const auto r1 = ex(3).transform(twice).transform(string); + static_assert(std::is_same>::value, ""); + EXPECT_EQ("6", *r1); + EXPECT_EQ("error", ex(TypeParam::unexpect(), "error").transform(twice).error()); + // A callable that returns void gives an expected. + int calls = 0; + const auto touch = [&calls](const int) { ++calls; }; + const auto r2 = ex(1).transform(touch); + static_assert(std::is_same>::value, ""); + EXPECT_TRUE(r2.has_value()); + EXPECT_EQ(1, calls); + EXPECT_FALSE(ex(TypeParam::unexpect(), "error").transform(touch).has_value()); + EXPECT_EQ(1, calls); + // cv-qualifiers are removed from the result. + const auto constant = [](const int x) -> const std::string { return std::to_string(x); }; + static_assert(std::is_same>::value, + ""); + // Value categories. + using iex = expected_t; + iex value(0); + const iex const_value(0); + EXPECT_EQ(as_int(category::lvalue), *value.transform(category_recorder{})); + EXPECT_EQ(as_int(category::const_lvalue), *const_value.transform(category_recorder{})); + EXPECT_EQ(as_int(category::rvalue), *std::move(value).transform(category_recorder{})); + EXPECT_EQ(as_int(category::const_rvalue), *std::move(const_value).transform(category_recorder{})); +} + +TYPED_TEST(test_pf23_expected_operations, transform_error) +{ + using ex = expected_t; + const auto string = [](const int e) { return std::to_string(e); }; + const auto r1 = ex(TypeParam::unexpect(), 4).transform_error(string); + static_assert(std::is_same>::value, ""); + EXPECT_EQ("4", r1.error()); + EXPECT_EQ(5, *ex(5).transform_error(string)); + ex error(TypeParam::unexpect(), 0); + const ex const_error(TypeParam::unexpect(), 0); + EXPECT_EQ(as_int(category::lvalue), error.transform_error(category_recorder{}).error()); + EXPECT_EQ(as_int(category::const_lvalue), const_error.transform_error(category_recorder{}).error()); + EXPECT_EQ(as_int(category::rvalue), std::move(error).transform_error(category_recorder{}).error()); + EXPECT_EQ(as_int(category::const_rvalue), std::move(const_error).transform_error(category_recorder{}).error()); + // The value is moved from an rvalue. + using mex = expected_t, int>; + mex value(TypeParam::in_place(), new int{7}); + const auto r2 = std::move(value).transform_error(string); + EXPECT_EQ(7, **r2); + EXPECT_EQ(nullptr, value->get()); +} + +TYPED_TEST(test_pf23_expected_operations, monadic_operations_on_void) +{ + using vex = expected_t; + using iex = expected_t; + int calls = 0; + auto bump = [&calls]() { return iex(++calls); }; + vex value; + vex error(TypeParam::unexpect(), 9); + EXPECT_EQ(1, *value.and_then(bump)); + EXPECT_EQ(9, error.and_then(bump).error()); + EXPECT_EQ(1, calls); + EXPECT_EQ(2, *value.transform([] { return 2; })); + EXPECT_EQ(9, error.transform([] { return 2; }).error()); + const auto nothing = [] {}; + static_assert(std::is_same::value, ""); + EXPECT_TRUE(value.transform(nothing).has_value()); + EXPECT_TRUE(value.or_else([](int) { return vex(TypeParam::unexpect(), 0); }).has_value()); + EXPECT_TRUE(error.or_else([](int) { return vex(); }).has_value()); + EXPECT_EQ(10, error.or_else([](int e) { return vex(TypeParam::unexpect(), e + 1); }).error()); + const auto string = [](int e) { return std::to_string(e); }; + EXPECT_TRUE(value.transform_error(string).has_value()); + EXPECT_EQ("9", error.transform_error(string).error()); + static_assert(std::is_same>::value, + ""); + // Value categories of the error. + const vex const_error(TypeParam::unexpect(), 0); + vex mutable_error(TypeParam::unexpect(), 0); + EXPECT_EQ(as_int(category::lvalue), mutable_error.transform_error(category_recorder{}).error()); + EXPECT_EQ(as_int(category::const_lvalue), const_error.transform_error(category_recorder{}).error()); + EXPECT_EQ(as_int(category::rvalue), std::move(mutable_error).transform_error(category_recorder{}).error()); + EXPECT_EQ(as_int(category::const_rvalue), std::move(const_error).transform_error(category_recorder{}).error()); + const auto void_recorder = [](auto&& e) { + return vex(TypeParam::unexpect(), std::is_const>::value ? 1 : 0); + }; + EXPECT_EQ(1, const_error.or_else(void_recorder).error()); + EXPECT_EQ(0, std::move(const_error).and_then([] { return vex(); }).error()); + EXPECT_EQ(0, std::move(mutable_error).transform([] { return 1; }).error()); +} + +TYPED_TEST(test_pf23_expected_operations, monadic_operations_every_category_and_state) +{ + using ex = expected_t; + for (const ex& source : {ex(3), ex(TypeParam::unexpect(), 4)}) + { + const bool has = source.has_value(); + for_each_category(source, [has](auto&& e) { + const auto r = std::forward(e).and_then([](const int v) { return ex(v + 1); }); + EXPECT_EQ(has, r.has_value()); + EXPECT_EQ(4, r.has_value() ? *r : r.error()); + }); + for_each_category(source, [has](auto&& e) { + const auto r = + std::forward(e).or_else([](const int v) { return ex(TypeParam::unexpect(), v + 2); }); + EXPECT_EQ(has, r.has_value()); + EXPECT_EQ(has ? 3 : 6, r.has_value() ? *r : r.error()); + }); + for_each_category(source, [has](auto&& e) { + const auto r = std::forward(e).transform([](const int v) { return v * 2; }); + EXPECT_EQ(has, r.has_value()); + EXPECT_EQ(has ? 6 : 4, r.has_value() ? *r : r.error()); + }); + for_each_category(source, [has](auto&& e) { + const auto r = std::forward(e).transform([](const int) {}); + EXPECT_EQ(has, r.has_value()); + }); + for_each_category(source, [has](auto&& e) { + const auto r = std::forward(e).transform_error([](const int v) { return v * 3; }); + EXPECT_EQ(has, r.has_value()); + EXPECT_EQ(has ? 3 : 12, r.has_value() ? *r : r.error()); + }); + } + using vex = expected_t; + for (const vex& source : {vex(), vex(TypeParam::unexpect(), 4)}) + { + const bool has = source.has_value(); + for_each_category(source, [has](auto&& e) { + const auto r = std::forward(e).and_then([] { return vex(); }); + EXPECT_EQ(has, r.has_value()); + }); + for_each_category(source, [has](auto&& e) { + const auto r = + std::forward(e).or_else([](const int v) { return vex(TypeParam::unexpect(), v + 2); }); + EXPECT_EQ(has, r.has_value()); + EXPECT_EQ(has ? 0 : 6, r.has_value() ? 0 : r.error()); + }); + for_each_category(source, [has](auto&& e) { + const auto r = std::forward(e).transform([] { return 2; }); + EXPECT_EQ(has, r.has_value()); + EXPECT_EQ(has ? 2 : 4, r.has_value() ? *r : r.error()); + }); + for_each_category(source, [has](auto&& e) { + const auto r = std::forward(e).transform([] {}); + EXPECT_EQ(has, r.has_value()); + }); + for_each_category(source, [has](auto&& e) { + const auto r = std::forward(e).transform_error([](const int v) { return v * 3; }); + EXPECT_EQ(has, r.has_value()); + EXPECT_EQ(has ? 0 : 12, r.has_value() ? 0 : r.error()); + }); + } +} + +TYPED_TEST(test_pf23_expected_operations, monadic_operations_invoke_members) +{ + using ex = expected_t; + const ex value(widget{4}); + EXPECT_EQ(4, *value.transform(&widget::get)); + using pex = expected_t; + const widget w{5}; + const pex pointer(&w); + EXPECT_EQ(5, *pointer.transform(&widget::get)); + using rex = expected_t, int>; + const rex ref(std::cref(w)); + EXPECT_EQ(5, *ref.transform(&widget::get)); + // The value category of a data member follows the object. + using iex = expected_t; + const auto rvalue = ex(widget{6}).and_then([](widget&& x) { return iex(std::move(x).value); }); + EXPECT_EQ(6, *rvalue); +} + +TYPED_TEST(test_pf23_expected_operations, monadic_operations_forward_the_callable) +{ + struct qualified_callable + { + int operator()(int) & noexcept + { + return 1; + } + int operator()(int) && noexcept + { + return 2; + } + }; + using ex = expected_t; + const ex value(0); + qualified_callable callable{}; + EXPECT_EQ(1, *value.transform(callable)); + EXPECT_EQ(2, *value.transform(qualified_callable{})); +} + +TYPED_TEST(test_pf23_expected_operations, transform_constructs_in_place) +{ + // transform() must not copy or move the new value: it is initialized from the result of the call. + using ex = expected_t; + value_counted::reset(); + { + const auto made = ex(3).transform([](const int x) { return value_counted{x}; }); + EXPECT_EQ(3, made->value); +#if __cplusplus >= CETL_CPP_STANDARD_17 + EXPECT_EQ(0, value_counted::stats().copy_ctor + value_counted::stats().move_ctor); +#endif + } +} + +// +----------------------------------------------------------------------+ +// | COMPARISON +// +----------------------------------------------------------------------+ + +TYPED_TEST(test_pf23_expected_operations, compare_expected) +{ + using ex = expected_t; + using lex = expected_t; + const ex one(1); + const ex error(TypeParam::unexpect(), "e"); + EXPECT_TRUE(one == ex(1)); + EXPECT_FALSE(one != ex(1)); + EXPECT_FALSE(one == ex(2)); + EXPECT_TRUE(one != ex(2)); + EXPECT_FALSE(one == error); + EXPECT_FALSE(error == one); + EXPECT_TRUE(error == ex(TypeParam::unexpect(), "e")); + EXPECT_FALSE(error == ex(TypeParam::unexpect(), "f")); + EXPECT_TRUE(one == lex(1L)); + EXPECT_TRUE(lex(1L) == one); + EXPECT_TRUE(error != lex(1L)); +} + +TYPED_TEST(test_pf23_expected_operations, compare_value) +{ + using ex = expected_t; + const ex one(1); + const ex error(TypeParam::unexpect(), 1); + EXPECT_TRUE(one == 1); + EXPECT_TRUE(1 == one); + EXPECT_TRUE(one == 1L); + EXPECT_FALSE(one != 1); + EXPECT_TRUE(one != 2); + EXPECT_TRUE(2 != one); + EXPECT_FALSE(error == 1); + EXPECT_FALSE(1 == error); + EXPECT_TRUE(error != 1); +} + +TYPED_TEST(test_pf23_expected_operations, compare_unexpected) +{ + using ex = expected_t; + using unex = unexpected_t; + const ex one(1); + const ex error(TypeParam::unexpect(), 1); + EXPECT_TRUE(error == unex(1)); + EXPECT_TRUE(unex(1) == error); + EXPECT_TRUE((error == unexpected_t(1L))); + EXPECT_FALSE(error == unex(2)); + EXPECT_TRUE(error != unex(2)); + EXPECT_TRUE(unex(2) != error); + EXPECT_FALSE(one == unex(1)); + EXPECT_TRUE(one != unex(1)); +} + +TYPED_TEST(test_pf23_expected_operations, compare_void) +{ + using vex = expected_t; + using unex = unexpected_t; + const vex value; + const vex error(TypeParam::unexpect(), 1); + EXPECT_TRUE(value == vex()); + EXPECT_TRUE((value == expected_t())); + EXPECT_FALSE(value == error); + EXPECT_FALSE(error == value); + EXPECT_TRUE(value != error); + EXPECT_TRUE(error == vex(TypeParam::unexpect(), 1)); + EXPECT_FALSE(error == vex(TypeParam::unexpect(), 2)); + EXPECT_TRUE(error == unex(1)); + EXPECT_TRUE(unex(1) == error); + EXPECT_TRUE(error != unex(2)); + EXPECT_TRUE(unex(2) != error); + EXPECT_FALSE(value == unex(1)); +} + +/// Detects whether `a == b` is well-formed. +template +struct is_equality_comparable : std::false_type +{}; +template +struct is_equality_comparable() == std::declval()))> + : std::true_type +{}; + +struct not_comparable +{}; + +TEST(test_pf23_expected_cetl, comparison_constraints) +{ + // The operators take part in overload resolution only where the comparisons they make are valid (P3379R0, a + // C++26 change). C++23 made these Mandates, and libc++ 23 still does, so this is not checked against std. + using ex = cetl::pf23::expected; + static_assert(!is_equality_comparable::value, ""); + static_assert(!is_equality_comparable::value, ""); + static_assert(is_equality_comparable>::value, ""); + static_assert(!is_equality_comparable>::value, ""); + using iex = cetl::pf23::expected; + using vex = cetl::pf23::expected; + static_assert(!is_equality_comparable::value, ""); + static_assert(!is_equality_comparable::value, ""); + static_assert(is_equality_comparable::value, ""); + static_assert(is_equality_comparable::value, ""); + static_assert(!is_equality_comparable::value, ""); + SUCCEED(); +} + +TEST(test_pf23_expected_cetl, invoke_member_pointers) +{ + // transform() cannot return a reference, so the data member forms of INVOKE are checked directly. + using cetl::pf23::detail::exp::invoke; + widget w{1}; + const widget cw{2}; + static_assert(std::is_same::value, ""); + static_assert(std::is_same::value, ""); + static_assert(std::is_same::value, ""); + static_assert(std::is_same::value, ""); + static_assert(std::is_same::value, ""); + static_assert(std::is_same::value, ""); + EXPECT_EQ(1, invoke(&widget::value, w)); + EXPECT_EQ(2, invoke(&widget::value, &cw)); + EXPECT_EQ(2, invoke(&widget::value, std::cref(cw))); + EXPECT_EQ(4, invoke(&widget::add, &cw, 2)); + EXPECT_EQ(3, invoke(&widget::add, std::ref(w), 2)); + EXPECT_EQ(5, invoke([](const int x) { return x; }, 5)); +} + +// +----------------------------------------------------------------------+ +// | UNEXPECTED +// +----------------------------------------------------------------------+ + +TYPED_TEST(test_pf23_expected_operations, unexpected) +{ + using unex = unexpected_t; + unex from_value(error_counted{1}); + const unex in_place(TypeParam::in_place(), 2, 3); + const unex from_list(TypeParam::in_place(), {1, 2}, 3); + EXPECT_EQ(1, from_value.error().value); + EXPECT_EQ(5, in_place.error().value); + EXPECT_EQ(5, from_list.error().value); + static_assert(std::is_same::value, ""); + static_assert(std::is_same::value, ""); + static_assert(std::is_same::value, ""); + static_assert(std::is_same::value, ""); + unex copy(from_value); + EXPECT_TRUE(copy == from_value); + EXPECT_FALSE(copy != from_value); + EXPECT_TRUE(copy != in_place); + copy = in_place; + EXPECT_EQ(5, copy.error().value); + copy.swap(from_value); + EXPECT_EQ(1, copy.error().value); + EXPECT_EQ(5, from_value.error().value); + swap(copy, from_value); + EXPECT_EQ(5, copy.error().value); + EXPECT_TRUE((unexpected_t(1) == unexpected_t(1L))); + // Construction from a value is explicit. + static_assert(!std::is_convertible>::value, ""); + static_assert(std::is_constructible, int>::value, ""); + static_assert(!std::is_default_constructible>::value, ""); +} + +#if defined(__cpp_deduction_guides) +TEST(test_pf23_expected_cetl, unexpected_deduction_guide) +{ + const cetl::pf23::unexpected deduced(3); + static_assert(std::is_same>::value, ""); + EXPECT_EQ(3, deduced.error()); +} +#endif + +// +----------------------------------------------------------------------+ +// | POLYFILL HEADER +// +----------------------------------------------------------------------+ + +TEST(test_pf23_expected_cetl, cetlpf) +{ +#if CETLVAST_HAS_STD_EXPECTED + static_assert(std::is_same, std::expected>::value, ""); + static_assert(std::is_same, std::unexpected>::value, ""); + static_assert(std::is_same::value, ""); +#else + static_assert(std::is_same, cetl::pf23::expected>::value, ""); + static_assert(std::is_same, cetl::pf23::unexpected>::value, ""); + static_assert(std::is_same::value, ""); +#endif + // The tag types from the polyfill headers work with whichever expected was selected. + const cetl::expected value(cetl::in_place, 1); + const cetl::expected error(cetl::unexpect, 2L); + EXPECT_EQ(1, *value); + EXPECT_EQ(2L, error.error()); +} + +} // namespace + +// NOLINTEND(*-use-after-move) diff --git a/include/cetl/cetl.hpp b/include/cetl/cetl.hpp index 7a5a10f5..62d8be22 100644 --- a/include/cetl/cetl.hpp +++ b/include/cetl/cetl.hpp @@ -34,6 +34,7 @@ /// - @subpage example_11_memory_resource_o1heap /// - @subpage example_12_buddy_memory_resource /// - @subpage example_13_saturating_arithmetic +/// - @subpage example_14_expected /// /// @page example_01_polyfill_20 Example 1: CETL C++20 Polyfill Header /// Full example for @ref cetl/pf20/cetlpf.hpp @@ -88,6 +89,10 @@ /// Full example for @ref cetl/pf26/cetlpf.hpp /// @include example_13_saturating_arithmetic.cpp /// +/// @page example_14_expected Example 14: Returning a value or an error with the C++23 polyfill header +/// Full example for @ref cetl/pf23/cetlpf.hpp +/// @include example_14_expected.cpp +/// #ifndef CETL_H_INCLUDED #define CETL_H_INCLUDED diff --git a/include/cetl/pf23/cetlpf.hpp b/include/cetl/pf23/cetlpf.hpp new file mode 100644 index 00000000..e0f1e6e0 --- /dev/null +++ b/include/cetl/pf23/cetlpf.hpp @@ -0,0 +1,75 @@ +/// @file +/// CETL polyfill header for C++23 types +/// +/// @copyright +/// Copyright (C) OpenCyphal Development Team +/// Copyright Amazon.com Inc. or its affiliates. +/// SPDX-License-Identifier: MIT +/// +/// @warning +/// This header violates AUTOSAR-14 M7-3-6, A17-0-1, and A16-0-1 (and possibly other rules). Don't use CETL polyfill +/// headers in AUTOSAR code. +/// +/// @warning +/// polyfill headers cannot be used if CETL_H_ERASE is defined. +/// +/// The types are selected by the library feature-test macro rather than by `__cplusplus`, because the standard +/// library's support lags the language mode: if `__cpp_lib_expected >= 202211L` (std::expected with the monadic +/// operations) the standard types are used, otherwise cetl::pf23 provides them. +/// +/// For example, a parser that reports why it failed without using exceptions: +/// +/// @snippet{trimleft} example_14_expected.cpp example_14_expected_parse +/// +/// and the monadic operations that chain such steps without checking each result by hand: +/// +/// @snippet{trimleft} example_14_expected.cpp example_14_expected_chain +/// +/// (@ref example_14_expected "See full example here...") +/// + +#ifndef CETL_PF23_H_INCLUDED +#define CETL_PF23_H_INCLUDED + +/// @namespace cetl::pf23 This namespace contains C++23 polyfill (pf) types. +/// The types within this namespace adhere to the C++23 specification and should be drop-in replaceable +/// with said standard types where they are available. CETL polyfill types may implement a sub-set of +/// the required functionality but they will not implement non-compliant functionality. +#include "cetl/cetl.hpp" +#include "cetl/pf20/cetlpf.hpp" + +#if (__cplusplus >= CETL_CPP_STANDARD_20) +# include // Defines __cpp_lib_expected where the standard library provides the feature. +#endif + +#if defined(__cpp_lib_expected) && (__cpp_lib_expected >= 202211L) && !defined(CETL_DOXYGEN) +# include + +namespace cetl +{ +// expected +using std::expected; +using std::unexpected; +using std::bad_expected_access; +using std::unexpect_t; +using std::unexpect; +} // namespace cetl + +#else +# include "cetl/pf23/expected.hpp" + +namespace cetl +{ +// expected +using cetl::pf23::expected; +using cetl::pf23::unexpected; +# if defined(__cpp_exceptions) || defined(CETL_DOXYGEN) +using cetl::pf23::bad_expected_access; +# endif +using cetl::pf23::unexpect_t; +using cetl::pf23::unexpect; +} // namespace cetl + +#endif + +#endif // CETL_PF23_H_INCLUDED diff --git a/include/cetl/pf23/expected.hpp b/include/cetl/pf23/expected.hpp new file mode 100644 index 00000000..056b3512 --- /dev/null +++ b/include/cetl/pf23/expected.hpp @@ -0,0 +1,2302 @@ +/// @file +/// Defines the C++23 std::expected type and several related entities. +/// +/// @copyright +/// Copyright (C) OpenCyphal Development Team +/// Copyright Amazon.com Inc. or its affiliates. +/// SPDX-License-Identifier: MIT + +#ifndef CETL_PF23_EXPECTED_HPP_INCLUDED +#define CETL_PF23_EXPECTED_HPP_INCLUDED + +#include +#include +#include + +#include // We need this even if exceptions are disabled for std::terminate. +#include +#include +#include +#include +#include +#include + +namespace cetl +{ +namespace pf23 +{ +template +class unexpected; + +template +class expected; + +/// An implementation of C++23 \c std::unexpect_t. +struct unexpect_t +{ + explicit unexpect_t() = default; +}; + +/// An implementation of C++23 \c std::unexpect. +constexpr unexpect_t unexpect{}; + +#if defined(__cpp_exceptions) || defined(CETL_DOXYGEN) +template +class bad_expected_access; + +/// A polyfill for \c std::bad_expected_access, the base class of every \ref bad_expected_access. +/// This is only available if exceptions are enabled (__cpp_exceptions is defined). +template <> +class bad_expected_access : public std::exception +{ +public: + CETL_NODISCARD const char* what() const noexcept override + { + return "bad_expected_access"; + } + +protected: + bad_expected_access() noexcept = default; + bad_expected_access(const bad_expected_access&) noexcept = default; + bad_expected_access(bad_expected_access&&) noexcept = default; + bad_expected_access& operator=(const bad_expected_access&) noexcept = default; + bad_expected_access& operator=(bad_expected_access&&) noexcept = default; + ~bad_expected_access() noexcept override = default; +}; + +/// A polyfill for \c std::bad_expected_access, thrown by expected::value() when the expected holds an error. +/// This is only available if exceptions are enabled (__cpp_exceptions is defined). +template +class bad_expected_access : public bad_expected_access +{ +public: + explicit bad_expected_access(E e) + : m_error(std::move(e)) + { + } + + /// The error that the expected held. + /// @{ + CETL_NODISCARD E& error() & noexcept + { + return m_error; + } + CETL_NODISCARD const E& error() const& noexcept + { + return m_error; + } + CETL_NODISCARD E&& error() && noexcept + { + return std::move(m_error); + } + CETL_NODISCARD const E&& error() const&& noexcept + { + return std::move(m_error); + } + /// @} + +private: + E m_error; +}; +#endif + +namespace detail +{ +namespace exp +{ +// In C++17 and later the polyfill takes std::in_place_t, which is what cetl::in_place from cetl/pf17/cetlpf.hpp is +// there, so code written against cetl/pf23/cetlpf.hpp compiles unchanged whether it gets this type or std::expected. +#if __cplusplus >= CETL_CPP_STANDARD_17 +using in_place_t = std::in_place_t; +#else +using in_place_t = cetl::pf17::in_place_t; +#endif + +template +using remove_cvref_t = std::remove_cv_t>; + +/// Makes a type depend on a template parameter so that a constraint on a class template parameter is checked during +/// overload resolution (SFINAE) instead of when the class is instantiated. +template +struct dependent +{ + using type = T; +}; +template +using dependent_t = typename dependent::type; + +template +struct is_unexpected : std::false_type +{}; +template +struct is_unexpected> : std::true_type +{}; + +template +struct is_expected : std::false_type +{}; +template +struct is_expected> : std::true_type +{}; + +/// True if E is a valid template argument for unexpected: a non-array, non-cv-qualified object type that is not a +/// specialization of unexpected. +template +constexpr bool is_valid_error = std::is_object::value && (!std::is_array::value) && (!std::is_const::value) && + (!std::is_volatile::value) && (!is_unexpected::value); + +/// True if T is a valid value type for expected: void or a non-array object type that is not in_place_t, unexpect_t or +/// a specialization of unexpected (all ignoring cv-qualifiers). +template +constexpr bool is_valid_value = std::is_void::value || (std::is_object::value && (!std::is_array::value) && + (!std::is_same, in_place_t>::value) && + (!std::is_same, unexpect_t>::value) && + (!is_unexpected>::value)); + +/// std::is_convertible as specified for C++17. +/// CAVEAT: C++14 has no guaranteed copy elision, so std::is_convertible is false whenever To cannot be moved, +/// even if From converts to it implicitly. Binding To&& sees the implicit conversion without the move. It cannot bind +/// an lvalue of To itself, but that case is covered by std::is_convertible. +#if __cplusplus >= CETL_CPP_STANDARD_17 +template +using is_convertible = std::is_convertible; +#else +template +using is_convertible = cetl::pf17::disjunction, std::is_convertible>; +#endif + +/// The converts-from-any-cvref exposition-only concept of the standard. +template +using converts_from_any_cvref = cetl::pf17::disjunction, + is_convertible, + std::is_constructible, + is_convertible, + std::is_constructible, + is_convertible, + std::is_constructible, + is_convertible>; + +/// True if unexpected is constructible from any cv-ref qualification of W. +template +using unexpected_from_any_cvref = cetl::pf17::disjunction, W&>, + std::is_constructible, W>, + std::is_constructible, const W&>, + std::is_constructible, const W>>; + +template +struct is_reference_wrapper : std::false_type +{}; +template +struct is_reference_wrapper> : std::true_type +{}; + +/// INVOKE as specified for C++17 std::invoke. It is always called qualified, because in C++17 and later an unqualified +/// call would also find std::invoke through ADL. +/// @{ +template +constexpr auto invoke(F&& f, Args&&... args) -> decltype(std::forward(f)(std::forward(args)...)) +{ + return std::forward(f)(std::forward(args)...); +} +template ::value && std::is_base_of>::value, int> = 0> +constexpr auto invoke(M C::* f, Obj&& obj, Args&&... args) + -> decltype((std::forward(obj).*f)(std::forward(args)...)) +{ + return (std::forward(obj).*f)(std::forward(args)...); +} +template ::value && is_reference_wrapper>::value, int> = 0> +constexpr auto invoke(M C::* f, Obj&& obj, Args&&... args) -> decltype((obj.get().*f)(std::forward(args)...)) +{ + return (obj.get().*f)(std::forward(args)...); +} +template ::value && (!std::is_base_of>::value) && + (!is_reference_wrapper>::value), + int> = 0> +constexpr auto invoke(M C::* f, Obj&& obj, Args&&... args) + -> decltype(((*std::forward(obj)).*f)(std::forward(args)...)) +{ + return ((*std::forward(obj)).*f)(std::forward(args)...); +} +template ::value) && std::is_base_of>::value, int> = 0> +constexpr auto invoke(M C::* f, Obj&& obj) -> decltype(std::forward(obj).*f) +{ + return std::forward(obj).*f; +} +template ::value) && is_reference_wrapper>::value, int> = 0> +constexpr auto invoke(M C::* f, Obj&& obj) -> decltype(obj.get().*f) +{ + return obj.get().*f; +} +template ::value) && (!std::is_base_of>::value) && + (!is_reference_wrapper>::value), + int> = 0> +constexpr auto invoke(M C::* f, Obj&& obj) -> decltype((*std::forward(obj)).*f) +{ + return (*std::forward(obj)).*f; +} +/// @} + +template +using invoke_result_t = decltype(detail::exp::invoke(std::declval(), std::declval()...)); + +/// Stands in for the value of expected so that it can share the storage of expected. +struct void_value final +{}; + +struct copy_tag final +{}; +struct invoke_value_tag final +{}; +struct invoke_error_tag final +{}; + +/// Constructs a union member in place. The member may be const-qualified. +template +void construct_member(T& member, Args&&... args) +{ + ::new (const_cast(static_cast(std::addressof(member)))) T(std::forward(args)...); +} + +template +void destroy_member(T& member) noexcept +{ + member.~T(); +} + +/// The reinit-expected exposition-only function of the standard: replaces oldval, the active member of the union, with +/// newval constructed from args. If the construction throws, oldval is restored. When the old member is the stand-in +/// for void there is nothing to restore, so newval is constructed directly as the standard specifies for +/// expected. +/// @{ +template +using reinit_strategy = + std::integral_constant::value || std::is_same::value) + ? 0 + : (std::is_nothrow_move_constructible::value ? 1 : 2)>; + +template +void reinit(std::integral_constant, New& newval, Old& oldval, Args&&... args) +{ + detail::exp::destroy_member(oldval); + detail::exp::construct_member(newval, std::forward(args)...); +} +template +void reinit(std::integral_constant, New& newval, Old& oldval, Args&&... args) +{ + New tmp(std::forward(args)...); + detail::exp::destroy_member(oldval); + detail::exp::construct_member(newval, std::move(tmp)); +} +template +void reinit(std::integral_constant, New& newval, Old& oldval, Args&&... args) +{ + Old tmp(std::move(oldval)); + detail::exp::destroy_member(oldval); +#if defined(__cpp_exceptions) + try + { + detail::exp::construct_member(newval, std::forward(args)...); + } catch (...) + { + detail::exp::construct_member(oldval, std::move(tmp)); + throw; + } +#else + detail::exp::construct_member(newval, std::forward(args)...); +#endif +} + +template +void reinit_expected(New& newval, Old& oldval, Args&&... args) +{ + detail::exp::reinit(reinit_strategy{}, newval, oldval, std::forward(args)...); +} +/// @} + +template +[[noreturn]] void throw_bad_expected_access(Err&& error) +{ +#if defined(__cpp_exceptions) + throw bad_expected_access(std::forward(error)); +#else + (void) error; + std::terminate(); +#endif +} + +/// DESTRUCTION POLICY +/// T is the value type, or void_value for expected. +template ::value && std::is_trivially_destructible::value> +struct base_destruction; +/// Trivially destructible case. +template +struct base_destruction +{ + template + constexpr explicit base_destruction(const in_place_t, Args&&... args) + : m_value(std::forward(args)...) + , m_has_value(true) + { + } + template + constexpr explicit base_destruction(const unexpect_t, Args&&... args) + : m_error(std::forward(args)...) + , m_has_value(false) + { + } + // The invoke constructors initialize the member from the result of the call directly, so that in C++17 and later a + // transform() result that cannot be moved is still constructed in place as the standard requires. + template + constexpr explicit base_destruction(const invoke_value_tag, F&& f, Args&&... args) + : m_value(detail::exp::invoke(std::forward(f), std::forward(args)...)) + , m_has_value(true) + { + } + template + constexpr explicit base_destruction(const invoke_error_tag, F&& f, Args&&... args) + : m_error(detail::exp::invoke(std::forward(f), std::forward(args)...)) + , m_has_value(false) + { + } + template + explicit base_destruction(const copy_tag, Other&& other) + : m_has_value(other.m_has_value) + { + if (m_has_value) + { + detail::exp::construct_member(m_value, std::forward(other).m_value); + } + else + { + detail::exp::construct_member(m_error, std::forward(other).m_error); + } + } + union + { + T m_value; + E m_error; + }; + bool m_has_value; +}; +/// Non-trivially destructible case. +template +struct base_destruction +{ + template + constexpr explicit base_destruction(const in_place_t, Args&&... args) + : m_value(std::forward(args)...) + , m_has_value(true) + { + } + template + constexpr explicit base_destruction(const unexpect_t, Args&&... args) + : m_error(std::forward(args)...) + , m_has_value(false) + { + } + template + constexpr explicit base_destruction(const invoke_value_tag, F&& f, Args&&... args) + : m_value(detail::exp::invoke(std::forward(f), std::forward(args)...)) + , m_has_value(true) + { + } + template + constexpr explicit base_destruction(const invoke_error_tag, F&& f, Args&&... args) + : m_error(detail::exp::invoke(std::forward(f), std::forward(args)...)) + , m_has_value(false) + { + } + template + explicit base_destruction(const copy_tag, Other&& other) + : m_has_value(other.m_has_value) + { + if (m_has_value) + { + detail::exp::construct_member(m_value, std::forward(other).m_value); + } + else + { + detail::exp::construct_member(m_error, std::forward(other).m_error); + } + } + constexpr base_destruction(const base_destruction&) = default; + constexpr base_destruction(base_destruction&&) = default; + constexpr base_destruction& operator=(const base_destruction&) = default; + constexpr base_destruction& operator=(base_destruction&&) = default; + ~base_destruction() noexcept + { + if (m_has_value) + { + detail::exp::destroy_member(m_value); + } + else + { + detail::exp::destroy_member(m_error); + } + } + union + { + T m_value; + E m_error; + }; + bool m_has_value; +}; + +/// COPY CONSTRUCTION POLICY +template ::value && std::is_trivially_copy_constructible::value, + bool = std::is_copy_constructible::value && std::is_copy_constructible::value> +struct base_copy_construction; +/// Trivially copy constructible case. +template +struct base_copy_construction : base_destruction +{ + using base_destruction::base_destruction; +}; +/// Non-trivially copy constructible case. +template +struct base_copy_construction : base_destruction +{ + using base_destruction::base_destruction; + base_copy_construction(const base_copy_construction& other) + : base_destruction(copy_tag{}, other) + { + } + constexpr base_copy_construction(base_copy_construction&&) = default; + constexpr base_copy_construction& operator=(const base_copy_construction&) = default; + constexpr base_copy_construction& operator=(base_copy_construction&&) = default; + ~base_copy_construction() noexcept = default; +}; +/// Disabled copy construction case. +template +struct base_copy_construction : base_destruction +{ + using base_destruction::base_destruction; + constexpr base_copy_construction(const base_copy_construction&) = delete; + constexpr base_copy_construction(base_copy_construction&&) = default; + constexpr base_copy_construction& operator=(const base_copy_construction&) = default; + constexpr base_copy_construction& operator=(base_copy_construction&&) = default; + ~base_copy_construction() noexcept = default; +}; + +/// MOVE CONSTRUCTION POLICY +template ::value && std::is_trivially_move_constructible::value, + bool = std::is_move_constructible::value && std::is_move_constructible::value> +struct base_move_construction; +/// Trivially move constructible case. +template +struct base_move_construction : base_copy_construction +{ + using base_copy_construction::base_copy_construction; +}; +/// Non-trivially move constructible case. +template +struct base_move_construction : base_copy_construction +{ + using base_copy_construction::base_copy_construction; + constexpr base_move_construction(const base_move_construction&) = default; + base_move_construction(base_move_construction&& other) noexcept(std::is_nothrow_move_constructible::value && + std::is_nothrow_move_constructible::value) + : base_copy_construction(copy_tag{}, std::move(other)) + { + } + constexpr base_move_construction& operator=(const base_move_construction&) = default; + constexpr base_move_construction& operator=(base_move_construction&&) = default; + ~base_move_construction() noexcept = default; +}; +/// Non-move-constructible case. As in optional, this deletion only mirrors the other disabled cases; the one that takes +/// effect is in the final layer, base_move_assignment. +template +struct base_move_construction : base_copy_construction +{ + using base_copy_construction::base_copy_construction; + constexpr base_move_construction(const base_move_construction&) = default; + constexpr base_move_construction(base_move_construction&&) = delete; + constexpr base_move_construction& operator=(const base_move_construction&) = default; + constexpr base_move_construction& operator=(base_move_construction&&) = default; + ~base_move_construction() noexcept = default; +}; + +/// True if either alternative can be moved without throwing, which the standard requires of every assignment that may +/// switch between the value and the error so that the strong exception guarantee can be given. +template +constexpr bool either_nothrow_move_constructible = + std::is_nothrow_move_constructible::value || std::is_nothrow_move_constructible::value; + +/// COPY ASSIGNMENT POLICY +template +constexpr bool copy_assignment_enabled = + std::is_copy_assignable::value && std::is_copy_constructible::value && std::is_copy_assignable::value && + std::is_copy_constructible::value && either_nothrow_move_constructible; +template && // + std::is_trivially_copy_assignable::value && std::is_trivially_copy_constructible::value && // + std::is_trivially_destructible::value && // + std::is_trivially_copy_assignable::value && std::is_trivially_copy_constructible::value && // + std::is_trivially_destructible::value, + bool = copy_assignment_enabled> +struct base_copy_assignment; +/// Trivially copy assignable case. +template +struct base_copy_assignment : base_move_construction +{ + using base_move_construction::base_move_construction; +}; +/// Non-trivially copy assignable case. +template +struct base_copy_assignment : base_move_construction +{ + using base_move_construction::base_move_construction; + constexpr base_copy_assignment(const base_copy_assignment&) = default; + constexpr base_copy_assignment(base_copy_assignment&&) = default; + base_copy_assignment& operator=(const base_copy_assignment& other) + { + if (this->m_has_value && other.m_has_value) + { + this->m_value = other.m_value; + } + else if (this->m_has_value) + { + detail::exp::reinit_expected(this->m_error, this->m_value, other.m_error); + this->m_has_value = false; + } + else if (other.m_has_value) + { + detail::exp::reinit_expected(this->m_value, this->m_error, other.m_value); + this->m_has_value = true; + } + else + { + this->m_error = other.m_error; + } + return *this; + } + constexpr base_copy_assignment& operator=(base_copy_assignment&&) = default; + ~base_copy_assignment() noexcept = default; +}; +/// Disabled copy assignment case. +template +struct base_copy_assignment : base_move_construction +{ + using base_move_construction::base_move_construction; + constexpr base_copy_assignment(const base_copy_assignment&) = default; + constexpr base_copy_assignment(base_copy_assignment&&) = default; + constexpr base_copy_assignment& operator=(const base_copy_assignment&) = delete; + constexpr base_copy_assignment& operator=(base_copy_assignment&&) = default; + ~base_copy_assignment() noexcept = default; +}; + +/// MOVE ASSIGNMENT POLICY +template +constexpr bool move_assignment_enabled = + std::is_move_assignable::value && std::is_move_constructible::value && std::is_move_assignable::value && + std::is_move_constructible::value && either_nothrow_move_constructible; +template && // + std::is_trivially_move_assignable::value && std::is_trivially_move_constructible::value && // + std::is_trivially_destructible::value && // + std::is_trivially_move_assignable::value && std::is_trivially_move_constructible::value && // + std::is_trivially_destructible::value, + bool = move_assignment_enabled, + bool = std::is_move_constructible::value && std::is_move_constructible::value> +struct base_move_assignment; +/// Trivially move assignable case. +template +struct base_move_assignment : base_copy_assignment +{ + using base_copy_assignment::base_copy_assignment; +}; +/// Non-trivially move assignable case. +template +struct base_move_assignment : base_copy_assignment +{ + using base_copy_assignment::base_copy_assignment; + constexpr base_move_assignment(const base_move_assignment&) = default; + constexpr base_move_assignment(base_move_assignment&&) = default; + constexpr base_move_assignment& operator=(const base_move_assignment&) = default; + base_move_assignment& operator=(base_move_assignment&& other) noexcept( + std::is_nothrow_move_assignable::value && std::is_nothrow_move_constructible::value && + std::is_nothrow_move_assignable::value && std::is_nothrow_move_constructible::value) + { + if (this->m_has_value && other.m_has_value) + { + this->m_value = std::move(other.m_value); + } + else if (this->m_has_value) + { + detail::exp::reinit_expected(this->m_error, this->m_value, std::move(other.m_error)); + this->m_has_value = false; + } + else if (other.m_has_value) + { + detail::exp::reinit_expected(this->m_value, this->m_error, std::move(other.m_value)); + this->m_has_value = true; + } + else + { + this->m_error = std::move(other.m_error); + } + return *this; + } + ~base_move_assignment() noexcept = default; +}; +/// Disabled move assignment case. +template +struct base_move_assignment : base_copy_assignment +{ + using base_copy_assignment::base_copy_assignment; + constexpr base_move_assignment(const base_move_assignment&) = default; + constexpr base_move_assignment(base_move_assignment&&) = default; + constexpr base_move_assignment& operator=(const base_move_assignment&) = default; + constexpr base_move_assignment& operator=(base_move_assignment&&) = delete; + ~base_move_assignment() noexcept = default; +}; +/// Keep the move constructor explicitly deleted in the final policy layer when T or E cannot be moved, so that the +/// defaulted move constructor of expected is defined as deleted and rvalues bind to the copy constructor instead. See +/// the matching comment in cetl/pf17/optional.hpp. +template +struct base_move_assignment : base_copy_assignment +{ + using base_copy_assignment::base_copy_assignment; + constexpr base_move_assignment(const base_move_assignment&) = default; + constexpr base_move_assignment(base_move_assignment&&) = delete; + constexpr base_move_assignment& operator=(const base_move_assignment&) = default; + constexpr base_move_assignment& operator=(base_move_assignment&&) = delete; + ~base_move_assignment() noexcept = default; +}; + +/// CONSTRAINTS +/// https://en.cppreference.com/w/cpp/utility/expected/expected +template +using enable_value_ctor = + cetl::pf17::conjunction, in_place_t>>, + cetl::pf17::negation, expected>>, + cetl::pf17::negation, unexpect_t>>, + cetl::pf17::negation>>, + std::is_constructible, + cetl::pf17::disjunction, bool>>, + cetl::pf17::negation>>>>; + +template +using enable_converting_ctor = + cetl::pf17::conjunction, + std::is_constructible, + cetl::pf17::disjunction, bool>, + cetl::pf17::negation>>>, + cetl::pf17::negation>>>; + +template +using enable_void_converting_ctor = + cetl::pf17::conjunction, + std::is_constructible, + cetl::pf17::negation>>>; + +/// https://en.cppreference.com/w/cpp/utility/expected/operator%3D +template +using enable_value_assignment = + cetl::pf17::conjunction, remove_cvref_t>>, + cetl::pf17::negation>>, + std::is_constructible, + std::is_assignable, + cetl::pf17::disjunction, + std::is_nothrow_move_constructible, + std::is_nothrow_move_constructible>>; + +/// T is void_value for expected, which makes the last condition true as the standard specifies for it. +template +using enable_unexpected_assignment = + cetl::pf17::conjunction, + std::is_assignable, + cetl::pf17::disjunction, + std::is_nothrow_move_constructible, + std::is_nothrow_move_constructible>>; + +/// https://en.cppreference.com/w/cpp/utility/expected/swap +template +using enable_swap = cetl::pf17::conjunction< + cetl::pf17::is_swappable, + cetl::pf17::is_swappable, + std::is_move_constructible, + std::is_move_constructible, + cetl::pf17::disjunction, std::is_nothrow_move_constructible>>; + +template +using enable_void_swap = cetl::pf17::conjunction, std::is_move_constructible>; + +/// The equality operators take part in overload resolution only if every comparison they make is well-formed and +/// yields something convertible to bool (P3379R0). +template +using enable_comparison = std::enable_if_t...>::value, bool>; + +} // namespace exp +} // namespace detail + +/// A strict implementation of C++23 \c std::unexpected: the error held by an \ref expected. +template +class unexpected +{ + static_assert(detail::exp::is_valid_error, + "E must be a non-array object type that is neither cv-qualified nor a specialization of unexpected."); + +public: + constexpr unexpected(const unexpected&) = default; + constexpr unexpected(unexpected&&) = default; + + /// Constructs the error from \p e. + template < + typename Err = E, + std::enable_if_t, unexpected>::value, int> = 0, + std::enable_if_t, detail::exp::in_place_t>::value, int> = 0, + std::enable_if_t::value, int> = 0> + constexpr explicit unexpected(Err&& e) noexcept(std::is_nothrow_constructible::value) + : m_error(std::forward(e)) + { + } + + /// Constructs the error in place from \p args. + template ::value, int> = 0> + constexpr explicit unexpected(const detail::exp::in_place_t, + Args&&... args) noexcept(std::is_nothrow_constructible::value) + : m_error(std::forward(args)...) + { + } + template &, Args...>::value, int> = 0> + constexpr explicit unexpected(const detail::exp::in_place_t, std::initializer_list il, Args&&... args) noexcept( + std::is_nothrow_constructible&, Args...>::value) + : m_error(il, std::forward(args)...) + { + } + + ~unexpected() = default; + + unexpected& operator=(const unexpected&) = default; + unexpected& operator=(unexpected&&) = default; + + /// The error. + /// @{ + CETL_NODISCARD constexpr const E& error() const& noexcept + { + return m_error; + } + CETL_NODISCARD constexpr E& error() & noexcept + { + return m_error; + } + CETL_NODISCARD constexpr const E&& error() const&& noexcept + { + return std::move(m_error); + } + CETL_NODISCARD constexpr E&& error() && noexcept + { + return std::move(m_error); + } + /// @} + + /// Swaps the errors. + void swap(unexpected& other) noexcept(cetl::pf17::is_nothrow_swappable::value) + { + static_assert(cetl::pf17::is_swappable::value, "E must be swappable."); + using std::swap; + swap(m_error, other.m_error); + } + + template >::value, int> = 0> + friend void swap(unexpected& x, unexpected& y) noexcept(noexcept(x.swap(y))) + { + x.swap(y); + } + + /// Compares the errors. + /// @{ + template + friend constexpr bool operator==(const unexpected& x, const unexpected& y) + { + return static_cast(x.error() == y.error()); + } + template + friend constexpr bool operator!=(const unexpected& x, const unexpected& y) + { + return !(x == y); + } + /// @} + +private: + E m_error; +}; + +#if defined(__cpp_deduction_guides) +template +unexpected(E) -> unexpected; +#endif + +/// A strict implementation of C++23 \c std::expected: either a value of type T or an error of type E. +/// +/// Differences from \c std::expected that are visible to a program: +/// - Copy and move assignment are trivial when T and E are trivially copyable, so +/// \c std::is_trivially_copy_assignable> can be true where the standard leaves it unspecified. +/// - In C++14 the members that cannot be constant expressions there (assignment, emplace(), swap(), and constructors +/// that convert from another expected) are not \c constexpr, and transform() and transform_error() require the new +/// value or error type to be move constructible, because C++14 has no guaranteed copy elision. +/// - In C++14 the in-place constructors take \c cetl::pf17::in_place_t; in C++17 and later they take +/// \c std::in_place_t. \c cetl::in_place from cetl/pf17/cetlpf.hpp is the right one in every case. +/// - cv-qualified void is not supported as the value type; plain void is. +/// - If exceptions are disabled, value() calls \c std::terminate instead of throwing bad_expected_access. +/// +/// @tparam T The value type: a non-array object type other than \c in_place_t, \ref unexpect_t or a specialization of +/// \ref unexpected, or void. +/// @tparam E The error type: any type that is a valid template argument for \ref unexpected. +template +class expected : private detail::exp::base_move_assignment +{ + template + friend class expected; + + using base = detail::exp::base_move_assignment; + + static_assert(std::is_object::value && (!std::is_array::value), + "T must be a non-array object type or (not cv-qualified) void."); + static_assert(!std::is_same, detail::exp::in_place_t>::value, "T must not be in_place_t."); + static_assert(!std::is_same, unexpect_t>::value, "T must not be unexpect_t."); + static_assert(!detail::exp::is_unexpected>::value, + "T must not be a specialization of unexpected."); + static_assert(detail::exp::is_valid_error, "E must be a valid template argument for unexpected."); + +public: + using value_type = T; + using error_type = E; + using unexpected_type = unexpected; + + template + using rebind = expected; + + /// Constructs an expected that holds a value-initialized T. + template >::value, int> = 0> + constexpr expected() noexcept(std::is_nothrow_default_constructible::value) + : base(detail::exp::in_place_t{}) + { + } + + /// Copy constructor. Deleted unless T and E are copy constructible; trivial if they are trivially so. + constexpr expected(const expected&) = default; + + /// Move constructor. Trivial if T and E are trivially move constructible. + constexpr expected(expected&&) noexcept(std::is_nothrow_move_constructible::value && + std::is_nothrow_move_constructible::value) = default; + + /// Converts from another expected. + /// @{ + template < + typename U, + typename G, + std::enable_if_t, + detail::exp::is_convertible, + detail::exp::is_convertible>::value, + int> = 0> + expected(const expected& other) // NOLINT(*-explicit-constructor) + : base(detail::exp::copy_tag{}, static_cast::base&>(other)) + { + } + template , + cetl::pf17::negation, + detail::exp::is_convertible>>>::value, + int> = 0> + explicit expected(const expected& other) + : base(detail::exp::copy_tag{}, static_cast::base&>(other)) + { + } + template , + detail::exp::is_convertible, + detail::exp::is_convertible>::value, + int> = 0> + expected(expected&& other) // NOLINT(*-explicit-constructor) + : base(detail::exp::copy_tag{}, static_cast::base&&>(other)) + { + } + template < + typename U, + typename G, + std::enable_if_t, + cetl::pf17::negation, + detail::exp::is_convertible>>>::value, + int> = 0> + explicit expected(expected&& other) + : base(detail::exp::copy_tag{}, static_cast::base&&>(other)) + { + } + /// @} + + /// Constructs the value from \p value. + /// @{ + template , + std::enable_if_t, + detail::exp::is_convertible>::value, + int> = 0> + constexpr expected(U&& value) // NOLINT(*-explicit-constructor) + noexcept(std::is_nothrow_constructible::value) + : base(detail::exp::in_place_t{}, std::forward(value)) + { + } + template , + std::enable_if_t, + cetl::pf17::negation>>::value, + int> = 0> + constexpr explicit expected(U&& value) noexcept(std::is_nothrow_constructible::value) + : base(detail::exp::in_place_t{}, std::forward(value)) + { + } + /// @} + + /// Constructs the error from an \ref unexpected. + /// @{ + template , + detail::exp::is_convertible>::value, + int> = 0> + constexpr expected(const unexpected& e) // NOLINT(*-explicit-constructor) + noexcept(std::is_nothrow_constructible::value) + : base(unexpect, e.error()) + { + } + template < + typename G, + std::enable_if_t, + cetl::pf17::negation>>::value, + int> = 0> + constexpr explicit expected(const unexpected& e) noexcept(std::is_nothrow_constructible::value) + : base(unexpect, e.error()) + { + } + template < + typename G, + std::enable_if_t, detail::exp::is_convertible>::value, + int> = 0> + constexpr expected(unexpected&& e) // NOLINT(*-explicit-constructor) + noexcept(std::is_nothrow_constructible::value) + : base(unexpect, std::move(e).error()) + { + } + template , + cetl::pf17::negation>>::value, + int> = 0> + constexpr explicit expected(unexpected&& e) noexcept(std::is_nothrow_constructible::value) + : base(unexpect, std::move(e).error()) + { + } + /// @} + + /// Constructs the value in place. + /// @{ + template ::value, int> = 0> + constexpr explicit expected(const detail::exp::in_place_t, + Args&&... args) noexcept(std::is_nothrow_constructible::value) + : base(detail::exp::in_place_t{}, std::forward(args)...) + { + } + template &, Args...>::value, int> = 0> + constexpr explicit expected(const detail::exp::in_place_t, std::initializer_list il, Args&&... args) noexcept( + std::is_nothrow_constructible&, Args...>::value) + : base(detail::exp::in_place_t{}, il, std::forward(args)...) + { + } + /// @} + + /// Constructs the error in place. + /// @{ + template ::value, int> = 0> + constexpr explicit expected(const unexpect_t, + Args&&... args) noexcept(std::is_nothrow_constructible::value) + : base(unexpect, std::forward(args)...) + { + } + template &, Args...>::value, int> = 0> + constexpr explicit expected(const unexpect_t, std::initializer_list il, Args&&... args) noexcept( + std::is_nothrow_constructible&, Args...>::value) + : base(unexpect, il, std::forward(args)...) + { + } + /// @} + + ~expected() = default; + + /// Copy assignment. Deleted unless T and E are copy constructible and copy assignable and at least one of them is + /// nothrow move constructible. If the assignment switches between value and error and constructing the new + /// alternative throws, the old one is restored (strong exception guarantee). + expected& operator=(const expected&) = default; + + /// Move assignment. Same constraints and guarantee as the copy assignment, for moving. + expected& operator=(expected&&) noexcept(std::is_nothrow_move_assignable::value && + std::is_nothrow_move_constructible::value && + std::is_nothrow_move_assignable::value && + std::is_nothrow_move_constructible::value) = default; + + /// Assigns a value. + template , + std::enable_if_t::value, int> = 0> + expected& operator=(U&& value) + { + if (has_value()) + { + this->m_value = std::forward(value); + } + else + { + detail::exp::reinit_expected(this->m_value, this->m_error, std::forward(value)); + this->m_has_value = true; + } + return *this; + } + + /// Assigns an error. + /// @{ + template ::value, int> = 0> + expected& operator=(const unexpected& e) + { + assign_error(e.error()); + return *this; + } + template ::value, int> = 0> + expected& operator=(unexpected&& e) + { + assign_error(std::move(e).error()); + return *this; + } + /// @} + + /// Destroys the value or the error and constructs a value in place. + /// @{ + template ::value, int> = 0> + T& emplace(Args&&... args) noexcept + { + destroy_active(); + detail::exp::construct_member(this->m_value, std::forward(args)...); + this->m_has_value = true; + return this->m_value; + } + template &, Args...>::value, int> = 0> + T& emplace(std::initializer_list il, Args&&... args) noexcept + { + destroy_active(); + detail::exp::construct_member(this->m_value, il, std::forward(args)...); + this->m_has_value = true; + return this->m_value; + } + /// @} + + /// Swaps the contents. If one holds a value and the other an error, the alternative that is nothrow move + /// constructible is moved first so that the swap gives the strong exception guarantee. + template , E>::value, int> = 0> + void swap(expected& other) noexcept(std::is_nothrow_move_constructible::value && + cetl::pf17::is_nothrow_swappable::value && + std::is_nothrow_move_constructible::value && + cetl::pf17::is_nothrow_swappable::value) + { + if (has_value() && other.has_value()) + { + using std::swap; + swap(this->m_value, other.m_value); + } + else if (has_value()) + { + swap_value_with_error(other, std::is_nothrow_move_constructible{}); + this->m_has_value = false; + other.m_has_value = true; + } + else if (other.has_value()) + { + other.swap(*this); + } + else + { + using std::swap; + swap(this->m_error, other.m_error); + } + } + + template , E>::value, int> = 0> + friend void swap(expected& x, expected& y) noexcept(noexcept(x.swap(y))) + { + x.swap(y); + } + + /// Unchecked access to the value. The behavior is undefined if the expected holds an error. + /// @{ + constexpr const T* operator->() const noexcept + { + CETL_DEBUG_ASSERT(has_value(), "CDE_expected_001: operator-> requires a value."); + return std::addressof(this->m_value); + } + constexpr T* operator->() noexcept + { + CETL_DEBUG_ASSERT(has_value(), "CDE_expected_002: operator-> requires a value."); + return std::addressof(this->m_value); + } + constexpr const T& operator*() const& noexcept + { + CETL_DEBUG_ASSERT(has_value(), "CDE_expected_003: operator* requires a value."); + return this->m_value; + } + constexpr T& operator*() & noexcept + { + CETL_DEBUG_ASSERT(has_value(), "CDE_expected_004: operator* requires a value."); + return this->m_value; + } + constexpr const T&& operator*() const&& noexcept + { + CETL_DEBUG_ASSERT(has_value(), "CDE_expected_005: operator* requires a value."); + return std::move(this->m_value); + } + constexpr T&& operator*() && noexcept + { + CETL_DEBUG_ASSERT(has_value(), "CDE_expected_006: operator* requires a value."); + return std::move(this->m_value); + } + /// @} + + /// True if the expected holds a value. + /// @{ + constexpr explicit operator bool() const noexcept + { + return has_value(); + } + CETL_NODISCARD constexpr bool has_value() const noexcept + { + return this->m_has_value; + } + /// @} + + /// Checked access to the value. + /// If the expected holds an error, the behavior depends on whether exception handling is enabled: + /// - If exceptions are enabled, a bad_expected_access holding a copy (or for rvalues a move) of the error is + /// thrown. + /// - If exceptions are disabled, std::terminate is called. + /// @{ + CETL_NODISCARD constexpr const T& value() const& + { + static_assert(std::is_copy_constructible::value, "value() requires a copy constructible error type."); + if (!has_value()) + { + detail::exp::throw_bad_expected_access(this->m_error); + } + return this->m_value; + } + CETL_NODISCARD constexpr T& value() & + { + static_assert(std::is_copy_constructible::value, "value() requires a copy constructible error type."); + if (!has_value()) + { + detail::exp::throw_bad_expected_access(static_cast(this->m_error)); + } + return this->m_value; + } + CETL_NODISCARD constexpr const T&& value() const&& + { + static_assert(std::is_copy_constructible::value && std::is_constructible::value, + "value() requires a copy constructible error type."); + if (!has_value()) + { + detail::exp::throw_bad_expected_access(std::move(this->m_error)); + } + return std::move(this->m_value); + } + CETL_NODISCARD constexpr T&& value() && + { + static_assert(std::is_copy_constructible::value && std::is_constructible::value, + "value() requires a copy and move constructible error type."); + if (!has_value()) + { + detail::exp::throw_bad_expected_access(std::move(this->m_error)); + } + return std::move(this->m_value); + } + /// @} + + /// Unchecked access to the error. The behavior is undefined if the expected holds a value. + /// @{ + CETL_NODISCARD constexpr const E& error() const& noexcept + { + CETL_DEBUG_ASSERT(!has_value(), "CDE_expected_007: error() requires an error."); + return this->m_error; + } + CETL_NODISCARD constexpr E& error() & noexcept + { + CETL_DEBUG_ASSERT(!has_value(), "CDE_expected_008: error() requires an error."); + return this->m_error; + } + CETL_NODISCARD constexpr const E&& error() const&& noexcept + { + CETL_DEBUG_ASSERT(!has_value(), "CDE_expected_009: error() requires an error."); + return std::move(this->m_error); + } + CETL_NODISCARD constexpr E&& error() && noexcept + { + CETL_DEBUG_ASSERT(!has_value(), "CDE_expected_010: error() requires an error."); + return std::move(this->m_error); + } + /// @} + + /// The value if there is one, otherwise \p default_value converted to T. + /// @{ + template > + CETL_NODISCARD constexpr T value_or(U&& default_value) const& + { + static_assert(std::is_copy_constructible::value, "value_or() requires a copy constructible value type."); + static_assert(detail::exp::is_convertible::value, "value_or() requires an argument convertible to T."); + return has_value() ? this->m_value : static_cast(std::forward(default_value)); + } + template > + CETL_NODISCARD constexpr T value_or(U&& default_value) && + { + static_assert(std::is_move_constructible::value, "value_or() requires a move constructible value type."); + static_assert(detail::exp::is_convertible::value, "value_or() requires an argument convertible to T."); + return has_value() ? std::move(this->m_value) : static_cast(std::forward(default_value)); + } + /// @} + + /// The error if there is one, otherwise \p default_error converted to E. + /// @{ + template + CETL_NODISCARD constexpr E error_or(G&& default_error) const& + { + static_assert(std::is_copy_constructible::value, "error_or() requires a copy constructible error type."); + static_assert(detail::exp::is_convertible::value, "error_or() requires an argument convertible to E."); + return has_value() ? static_cast(std::forward(default_error)) : this->m_error; + } + template + CETL_NODISCARD constexpr E error_or(G&& default_error) && + { + static_assert(std::is_move_constructible::value, "error_or() requires a move constructible error type."); + static_assert(detail::exp::is_convertible::value, "error_or() requires an argument convertible to E."); + return has_value() ? static_cast(std::forward(default_error)) : std::move(this->m_error); + } + /// @} + + /// If there is a value, returns the result of invoking \p f with it, which must be an expected with the same error + /// type; otherwise returns the error. + /// @{ + template >::value, int> = 0> + constexpr auto and_then(F&& f) & + { + using U = detail::exp::remove_cvref_t>; + static_assert(detail::exp::is_expected::value, "and_then() requires f to return an expected."); + static_assert(std::is_same::value, + "and_then() requires f to return an expected with the same error type."); + if (has_value()) + { + return detail::exp::invoke(std::forward(f), this->m_value); + } + return U(unexpect, this->m_error); + } + template >::value, int> = 0> + constexpr auto and_then(F&& f) const& + { + using U = detail::exp::remove_cvref_t>; + static_assert(detail::exp::is_expected::value, "and_then() requires f to return an expected."); + static_assert(std::is_same::value, + "and_then() requires f to return an expected with the same error type."); + if (has_value()) + { + return detail::exp::invoke(std::forward(f), this->m_value); + } + return U(unexpect, this->m_error); + } + template >::value, int> = 0> + constexpr auto and_then(F&& f) && + { + using U = detail::exp::remove_cvref_t>; + static_assert(detail::exp::is_expected::value, "and_then() requires f to return an expected."); + static_assert(std::is_same::value, + "and_then() requires f to return an expected with the same error type."); + if (has_value()) + { + return detail::exp::invoke(std::forward(f), std::move(this->m_value)); + } + return U(unexpect, std::move(this->m_error)); + } + template >::value, int> = 0> + constexpr auto and_then(F&& f) const&& + { + using U = detail::exp::remove_cvref_t>; + static_assert(detail::exp::is_expected::value, "and_then() requires f to return an expected."); + static_assert(std::is_same::value, + "and_then() requires f to return an expected with the same error type."); + if (has_value()) + { + return detail::exp::invoke(std::forward(f), std::move(this->m_value)); + } + return U(unexpect, std::move(this->m_error)); + } + /// @} + + /// If there is an error, returns the result of invoking \p f with it, which must be an expected with the same value + /// type; otherwise returns the value. + /// @{ + template >::value, int> = 0> + constexpr auto or_else(F&& f) & + { + using G = detail::exp::remove_cvref_t>; + static_assert(detail::exp::is_expected::value, "or_else() requires f to return an expected."); + static_assert(std::is_same::value, + "or_else() requires f to return an expected with the same value type."); + if (has_value()) + { + return G(detail::exp::in_place_t{}, this->m_value); + } + return detail::exp::invoke(std::forward(f), this->m_error); + } + template >::value, int> = 0> + constexpr auto or_else(F&& f) const& + { + using G = detail::exp::remove_cvref_t>; + static_assert(detail::exp::is_expected::value, "or_else() requires f to return an expected."); + static_assert(std::is_same::value, + "or_else() requires f to return an expected with the same value type."); + if (has_value()) + { + return G(detail::exp::in_place_t{}, this->m_value); + } + return detail::exp::invoke(std::forward(f), this->m_error); + } + template >::value, int> = 0> + constexpr auto or_else(F&& f) && + { + using G = detail::exp::remove_cvref_t>; + static_assert(detail::exp::is_expected::value, "or_else() requires f to return an expected."); + static_assert(std::is_same::value, + "or_else() requires f to return an expected with the same value type."); + if (has_value()) + { + return G(detail::exp::in_place_t{}, std::move(this->m_value)); + } + return detail::exp::invoke(std::forward(f), std::move(this->m_error)); + } + template >::value, int> = 0> + constexpr auto or_else(F&& f) const&& + { + using G = detail::exp::remove_cvref_t>; + static_assert(detail::exp::is_expected::value, "or_else() requires f to return an expected."); + static_assert(std::is_same::value, + "or_else() requires f to return an expected with the same value type."); + if (has_value()) + { + return G(detail::exp::in_place_t{}, std::move(this->m_value)); + } + return detail::exp::invoke(std::forward(f), std::move(this->m_error)); + } + /// @} + + /// If there is a value, returns an expected holding the result of invoking \p f with it (or a value of void if + /// \p f returns void); otherwise returns the error. + /// @{ + template >::value, int> = 0> + constexpr auto transform(F&& f) & + { + using U = std::remove_cv_t>; + + static_assert(detail::exp::is_valid_value, "transform() requires f to return a valid value type."); + if (has_value()) + { + return expected(detail::exp::invoke_value_tag{}, std::forward(f), this->m_value); + } + return expected(unexpect, this->m_error); + } + template >::value, int> = 0> + constexpr auto transform(F&& f) const& + { + using U = std::remove_cv_t>; + + static_assert(detail::exp::is_valid_value, "transform() requires f to return a valid value type."); + if (has_value()) + { + return expected(detail::exp::invoke_value_tag{}, std::forward(f), this->m_value); + } + return expected(unexpect, this->m_error); + } + template >::value, int> = 0> + constexpr auto transform(F&& f) && + { + using U = std::remove_cv_t>; + + static_assert(detail::exp::is_valid_value, "transform() requires f to return a valid value type."); + if (has_value()) + { + return expected(detail::exp::invoke_value_tag{}, std::forward(f), std::move(this->m_value)); + } + return expected(unexpect, std::move(this->m_error)); + } + template >::value, int> = 0> + constexpr auto transform(F&& f) const&& + { + using U = std::remove_cv_t>; + + static_assert(detail::exp::is_valid_value, "transform() requires f to return a valid value type."); + if (has_value()) + { + return expected(detail::exp::invoke_value_tag{}, std::forward(f), std::move(this->m_value)); + } + return expected(unexpect, std::move(this->m_error)); + } + /// @} + + /// If there is an error, returns an expected holding the result of invoking \p f with it; otherwise returns the + /// value. + /// @{ + template >::value, int> = 0> + constexpr auto transform_error(F&& f) & + { + using G = std::remove_cv_t>; + + static_assert(detail::exp::is_valid_error, "transform_error() requires f to return a valid error type."); + if (has_value()) + { + return expected(detail::exp::in_place_t{}, this->m_value); + } + return expected(detail::exp::invoke_error_tag{}, std::forward(f), this->m_error); + } + template >::value, int> = 0> + constexpr auto transform_error(F&& f) const& + { + using G = std::remove_cv_t>; + + static_assert(detail::exp::is_valid_error, "transform_error() requires f to return a valid error type."); + if (has_value()) + { + return expected(detail::exp::in_place_t{}, this->m_value); + } + return expected(detail::exp::invoke_error_tag{}, std::forward(f), this->m_error); + } + template >::value, int> = 0> + constexpr auto transform_error(F&& f) && + { + using G = std::remove_cv_t>; + + static_assert(detail::exp::is_valid_error, "transform_error() requires f to return a valid error type."); + if (has_value()) + { + return expected(detail::exp::in_place_t{}, std::move(this->m_value)); + } + return expected(detail::exp::invoke_error_tag{}, std::forward(f), std::move(this->m_error)); + } + template >::value, int> = 0> + constexpr auto transform_error(F&& f) const&& + { + using G = std::remove_cv_t>; + + static_assert(detail::exp::is_valid_error, "transform_error() requires f to return a valid error type."); + if (has_value()) + { + return expected(detail::exp::in_place_t{}, std::move(this->m_value)); + } + return expected(detail::exp::invoke_error_tag{}, std::forward(f), std::move(this->m_error)); + } + /// @} + + /// Compares with another expected: equal if both hold equal values or both hold equal errors. + /// @{ + template ::value, int> = 0> + friend constexpr detail::exp::enable_comparison() == std::declval()), + decltype(std::declval() == std::declval())> + operator==(const expected& x, const expected& y) + { + return (x.has_value() == y.has_value()) && + (x.has_value() ? static_cast(*x == *y) : static_cast(x.error() == y.error())); + } + template ::value, int> = 0> + friend constexpr detail::exp::enable_comparison() == std::declval()), + decltype(std::declval() == std::declval())> + operator!=(const expected& x, const expected& y) + { + return !(x == y); + } + /// @} + + /// Compares with a value: equal if the expected holds a value equal to \p v. + /// @{ + template ::value, int> = 0> + friend constexpr detail::exp::enable_comparison() == std::declval())> + operator==(const expected& x, const T2& v) + { + return x.has_value() && static_cast(*x == v); + } + template ::value, int> = 0> + friend constexpr detail::exp::enable_comparison() == std::declval())> + operator==(const T2& v, const expected& x) + { + return x.has_value() && static_cast(*x == v); + } + template ::value, int> = 0> + friend constexpr detail::exp::enable_comparison() == std::declval())> + operator!=(const expected& x, const T2& v) + { + return !(x == v); + } + template ::value, int> = 0> + friend constexpr detail::exp::enable_comparison() == std::declval())> + operator!=(const T2& v, const expected& x) + { + return !(x == v); + } + /// @} + + /// Compares with an \ref unexpected. They are equal if the expected holds an error equal to the one in \p e. + /// @{ + template + friend constexpr detail::exp::enable_comparison() == std::declval())> + operator==(const expected& x, const unexpected& e) + { + return (!x.has_value()) && static_cast(x.error() == e.error()); + } + template + friend constexpr detail::exp::enable_comparison() == std::declval())> + operator==(const unexpected& e, const expected& x) + { + return (!x.has_value()) && static_cast(x.error() == e.error()); + } + template + friend constexpr detail::exp::enable_comparison() == std::declval())> + operator!=(const expected& x, const unexpected& e) + { + return !(x == e); + } + template + friend constexpr detail::exp::enable_comparison() == std::declval())> + operator!=(const unexpected& e, const expected& x) + { + return !(x == e); + } + /// @} + +private: + template + constexpr expected(const detail::exp::invoke_value_tag, F&& f, Args&&... args) + : base(detail::exp::invoke_value_tag{}, std::forward(f), std::forward(args)...) + { + } + template + constexpr expected(const detail::exp::invoke_error_tag, F&& f, Args&&... args) + : base(detail::exp::invoke_error_tag{}, std::forward(f), std::forward(args)...) + { + } + + template + void assign_error(G&& error) + { + if (has_value()) + { + detail::exp::reinit_expected(this->m_error, this->m_value, std::forward(error)); + this->m_has_value = false; + } + else + { + this->m_error = std::forward(error); + } + } + + void destroy_active() noexcept + { + if (has_value()) + { + detail::exp::destroy_member(this->m_value); + } + else + { + detail::exp::destroy_member(this->m_error); + } + } + + /// Moves this value into other and other's error into this. E can be moved without throwing, so it is set aside + /// first and put back if moving the value throws. + void swap_value_with_error(expected& other, std::true_type) + { + E tmp(std::move(other.m_error)); + detail::exp::destroy_member(other.m_error); +#if defined(__cpp_exceptions) + try + { + detail::exp::construct_member(other.m_value, std::move(this->m_value)); + } catch (...) + { + detail::exp::construct_member(other.m_error, std::move(tmp)); + throw; + } +#else + detail::exp::construct_member(other.m_value, std::move(this->m_value)); +#endif + detail::exp::destroy_member(this->m_value); + detail::exp::construct_member(this->m_error, std::move(tmp)); + } + /// As above, but only T can be moved without throwing, so the value is set aside first. + void swap_value_with_error(expected& other, std::false_type) + { + T tmp(std::move(this->m_value)); + detail::exp::destroy_member(this->m_value); +#if defined(__cpp_exceptions) + try + { + detail::exp::construct_member(this->m_error, std::move(other.m_error)); + } catch (...) + { + detail::exp::construct_member(this->m_value, std::move(tmp)); + throw; + } +#else + detail::exp::construct_member(this->m_error, std::move(other.m_error)); +#endif + detail::exp::destroy_member(other.m_error); + detail::exp::construct_member(other.m_value, std::move(tmp)); + } +}; + +/// A strict implementation of C++23 \c std::expected: either nothing (success) or an error of type E. +/// See the primary template for the differences from \c std::expected. +template +class expected : private detail::exp::base_move_assignment +{ + template + friend class expected; + + using base = detail::exp::base_move_assignment; + + static_assert(detail::exp::is_valid_error, "E must be a valid template argument for unexpected."); + +public: + using value_type = void; + using error_type = E; + using unexpected_type = unexpected; + + template + using rebind = expected; + + /// Constructs an expected that holds a value. + constexpr expected() noexcept + : base(detail::exp::in_place_t{}) + { + } + + /// Copy constructor. Deleted unless E is copy constructible; trivial if it is trivially so. + constexpr expected(const expected&) = default; + + /// Move constructor. Trivial if E is trivially move constructible. + constexpr expected(expected&&) noexcept(std::is_nothrow_move_constructible::value) = default; + + /// Converts from another expected with a void value type. + /// @{ + template , + detail::exp::is_convertible>::value, + int> = 0> + expected(const expected& other) // NOLINT(*-explicit-constructor) + : base(detail::exp::copy_tag{}, static_cast::base&>(other)) + { + } + template < + typename U, + typename G, + std::enable_if_t, + cetl::pf17::negation>>::value, + int> = 0> + explicit expected(const expected& other) + : base(detail::exp::copy_tag{}, static_cast::base&>(other)) + { + } + template , + detail::exp::is_convertible>::value, + int> = 0> + expected(expected&& other) // NOLINT(*-explicit-constructor) + : base(detail::exp::copy_tag{}, static_cast::base&&>(other)) + { + } + template , + cetl::pf17::negation>>::value, + int> = 0> + explicit expected(expected&& other) + : base(detail::exp::copy_tag{}, static_cast::base&&>(other)) + { + } + /// @} + + /// Constructs the error from an \ref unexpected. + /// @{ + template , + detail::exp::is_convertible>::value, + int> = 0> + constexpr expected(const unexpected& e) // NOLINT(*-explicit-constructor) + noexcept(std::is_nothrow_constructible::value) + : base(unexpect, e.error()) + { + } + template < + typename G, + std::enable_if_t, + cetl::pf17::negation>>::value, + int> = 0> + constexpr explicit expected(const unexpected& e) noexcept(std::is_nothrow_constructible::value) + : base(unexpect, e.error()) + { + } + template < + typename G, + std::enable_if_t, detail::exp::is_convertible>::value, + int> = 0> + constexpr expected(unexpected&& e) // NOLINT(*-explicit-constructor) + noexcept(std::is_nothrow_constructible::value) + : base(unexpect, std::move(e).error()) + { + } + template , + cetl::pf17::negation>>::value, + int> = 0> + constexpr explicit expected(unexpected&& e) noexcept(std::is_nothrow_constructible::value) + : base(unexpect, std::move(e).error()) + { + } + /// @} + + /// Constructs an expected that holds a value. + constexpr explicit expected(const detail::exp::in_place_t) noexcept + : base(detail::exp::in_place_t{}) + { + } + + /// Constructs the error in place. + /// @{ + template ::value, int> = 0> + constexpr explicit expected(const unexpect_t, + Args&&... args) noexcept(std::is_nothrow_constructible::value) + : base(unexpect, std::forward(args)...) + { + } + template &, Args...>::value, int> = 0> + constexpr explicit expected(const unexpect_t, std::initializer_list il, Args&&... args) noexcept( + std::is_nothrow_constructible&, Args...>::value) + : base(unexpect, il, std::forward(args)...) + { + } + /// @} + + ~expected() = default; + + /// Copy assignment. Deleted unless E is copy constructible and copy assignable. + expected& operator=(const expected&) = default; + + /// Move assignment. Deleted unless E is move constructible and move assignable. + expected& operator=(expected&&) noexcept(std::is_nothrow_move_assignable::value && + std::is_nothrow_move_constructible::value) = default; + + /// Assigns an error. + /// @{ + template ::value, + int> = 0> + expected& operator=(const unexpected& e) + { + assign_error(e.error()); + return *this; + } + template < + typename G, + std::enable_if_t::value, int> = 0> + expected& operator=(unexpected&& e) + { + assign_error(std::move(e).error()); + return *this; + } + /// @} + + /// Destroys the error, if any, so that the expected holds a value. + void emplace() noexcept + { + if (!has_value()) + { + detail::exp::destroy_member(this->m_error); + detail::exp::construct_member(this->m_value); + this->m_has_value = true; + } + } + + /// Swaps the contents. + template >::value, int> = 0> + void swap(expected& other) noexcept(std::is_nothrow_move_constructible::value && + cetl::pf17::is_nothrow_swappable::value) + { + if (has_value() && other.has_value()) + { + // Nothing to swap. + } + else if (has_value()) + { + detail::exp::construct_member(this->m_error, std::move(other.m_error)); + detail::exp::destroy_member(other.m_error); + detail::exp::construct_member(other.m_value); + this->m_has_value = false; + other.m_has_value = true; + } + else if (other.has_value()) + { + other.swap(*this); + } + else + { + using std::swap; + swap(this->m_error, other.m_error); + } + } + + template >::value, int> = 0> + friend void swap(expected& x, expected& y) noexcept(noexcept(x.swap(y))) + { + x.swap(y); + } + + /// True if the expected holds a value. + /// @{ + constexpr explicit operator bool() const noexcept + { + return has_value(); + } + CETL_NODISCARD constexpr bool has_value() const noexcept + { + return this->m_has_value; + } + /// @} + + /// Does nothing. The behavior is undefined if the expected holds an error. + constexpr void operator*() const noexcept + { + CETL_DEBUG_ASSERT(has_value(), "CDE_expected_011: operator* requires a value."); + } + + /// Checked access to the value: does nothing if there is one. + /// If the expected holds an error, the behavior depends on whether exception handling is enabled: + /// - If exceptions are enabled, a bad_expected_access holding a copy (or for rvalues a move) of the error is + /// thrown. + /// - If exceptions are disabled, std::terminate is called. + /// @{ + constexpr void value() const& + { + static_assert(std::is_copy_constructible::value, "value() requires a copy constructible error type."); + if (!has_value()) + { + detail::exp::throw_bad_expected_access(this->m_error); + } + } + constexpr void value() && + { + static_assert(std::is_copy_constructible::value && std::is_move_constructible::value, + "value() requires a copy and move constructible error type."); + if (!has_value()) + { + detail::exp::throw_bad_expected_access(std::move(this->m_error)); + } + } + /// @} + + /// Unchecked access to the error. The behavior is undefined if the expected holds a value. + /// @{ + CETL_NODISCARD constexpr const E& error() const& noexcept + { + CETL_DEBUG_ASSERT(!has_value(), "CDE_expected_012: error() requires an error."); + return this->m_error; + } + CETL_NODISCARD constexpr E& error() & noexcept + { + CETL_DEBUG_ASSERT(!has_value(), "CDE_expected_013: error() requires an error."); + return this->m_error; + } + CETL_NODISCARD constexpr const E&& error() const&& noexcept + { + CETL_DEBUG_ASSERT(!has_value(), "CDE_expected_014: error() requires an error."); + return std::move(this->m_error); + } + CETL_NODISCARD constexpr E&& error() && noexcept + { + CETL_DEBUG_ASSERT(!has_value(), "CDE_expected_015: error() requires an error."); + return std::move(this->m_error); + } + /// @} + + /// The error if there is one, otherwise \p default_error converted to E. + /// @{ + template + CETL_NODISCARD constexpr E error_or(G&& default_error) const& + { + static_assert(std::is_copy_constructible::value, "error_or() requires a copy constructible error type."); + static_assert(detail::exp::is_convertible::value, "error_or() requires an argument convertible to E."); + return has_value() ? static_cast(std::forward(default_error)) : this->m_error; + } + template + CETL_NODISCARD constexpr E error_or(G&& default_error) && + { + static_assert(std::is_move_constructible::value, "error_or() requires a move constructible error type."); + static_assert(detail::exp::is_convertible::value, "error_or() requires an argument convertible to E."); + return has_value() ? static_cast(std::forward(default_error)) : std::move(this->m_error); + } + /// @} + + /// If there is a value, returns the result of invoking \p f, which must be an expected with the same error type; + /// otherwise returns the error. + /// @{ + template >::value, int> = 0> + constexpr auto and_then(F&& f) & + { + using U = detail::exp::remove_cvref_t>; + static_assert(detail::exp::is_expected::value, "and_then() requires f to return an expected."); + static_assert(std::is_same::value, + "and_then() requires f to return an expected with the same error type."); + if (has_value()) + { + return detail::exp::invoke(std::forward(f)); + } + return U(unexpect, this->m_error); + } + template >::value, int> = 0> + constexpr auto and_then(F&& f) const& + { + using U = detail::exp::remove_cvref_t>; + static_assert(detail::exp::is_expected::value, "and_then() requires f to return an expected."); + static_assert(std::is_same::value, + "and_then() requires f to return an expected with the same error type."); + if (has_value()) + { + return detail::exp::invoke(std::forward(f)); + } + return U(unexpect, this->m_error); + } + template >::value, int> = 0> + constexpr auto and_then(F&& f) && + { + using U = detail::exp::remove_cvref_t>; + static_assert(detail::exp::is_expected::value, "and_then() requires f to return an expected."); + static_assert(std::is_same::value, + "and_then() requires f to return an expected with the same error type."); + if (has_value()) + { + return detail::exp::invoke(std::forward(f)); + } + return U(unexpect, std::move(this->m_error)); + } + template >::value, int> = 0> + constexpr auto and_then(F&& f) const&& + { + using U = detail::exp::remove_cvref_t>; + static_assert(detail::exp::is_expected::value, "and_then() requires f to return an expected."); + static_assert(std::is_same::value, + "and_then() requires f to return an expected with the same error type."); + if (has_value()) + { + return detail::exp::invoke(std::forward(f)); + } + return U(unexpect, std::move(this->m_error)); + } + /// @} + + /// If there is an error, returns the result of invoking \p f with it, which must be an expected with a void value + /// type; otherwise returns a value. + /// @{ + template + constexpr auto or_else(F&& f) & + { + using G = detail::exp::remove_cvref_t>; + static_assert(detail::exp::is_expected::value, "or_else() requires f to return an expected."); + static_assert(std::is_same::value, + "or_else() requires f to return an expected with the same value type."); + if (has_value()) + { + return G(); + } + return detail::exp::invoke(std::forward(f), this->m_error); + } + template + constexpr auto or_else(F&& f) const& + { + using G = detail::exp::remove_cvref_t>; + static_assert(detail::exp::is_expected::value, "or_else() requires f to return an expected."); + static_assert(std::is_same::value, + "or_else() requires f to return an expected with the same value type."); + if (has_value()) + { + return G(); + } + return detail::exp::invoke(std::forward(f), this->m_error); + } + template + constexpr auto or_else(F&& f) && + { + using G = detail::exp::remove_cvref_t>; + static_assert(detail::exp::is_expected::value, "or_else() requires f to return an expected."); + static_assert(std::is_same::value, + "or_else() requires f to return an expected with the same value type."); + if (has_value()) + { + return G(); + } + return detail::exp::invoke(std::forward(f), std::move(this->m_error)); + } + template + constexpr auto or_else(F&& f) const&& + { + using G = detail::exp::remove_cvref_t>; + static_assert(detail::exp::is_expected::value, "or_else() requires f to return an expected."); + static_assert(std::is_same::value, + "or_else() requires f to return an expected with the same value type."); + if (has_value()) + { + return G(); + } + return detail::exp::invoke(std::forward(f), std::move(this->m_error)); + } + /// @} + + /// If there is a value, returns an expected holding the result of invoking \p f (or a value of void if \p f + /// returns void); otherwise returns the error. + /// @{ + template >::value, int> = 0> + constexpr auto transform(F&& f) & + { + using U = std::remove_cv_t>; + + static_assert(detail::exp::is_valid_value, "transform() requires f to return a valid value type."); + if (has_value()) + { + return expected(detail::exp::invoke_value_tag{}, std::forward(f)); + } + return expected(unexpect, this->m_error); + } + template >::value, int> = 0> + constexpr auto transform(F&& f) const& + { + using U = std::remove_cv_t>; + + static_assert(detail::exp::is_valid_value, "transform() requires f to return a valid value type."); + if (has_value()) + { + return expected(detail::exp::invoke_value_tag{}, std::forward(f)); + } + return expected(unexpect, this->m_error); + } + template >::value, int> = 0> + constexpr auto transform(F&& f) && + { + using U = std::remove_cv_t>; + + static_assert(detail::exp::is_valid_value, "transform() requires f to return a valid value type."); + if (has_value()) + { + return expected(detail::exp::invoke_value_tag{}, std::forward(f)); + } + return expected(unexpect, std::move(this->m_error)); + } + template >::value, int> = 0> + constexpr auto transform(F&& f) const&& + { + using U = std::remove_cv_t>; + + static_assert(detail::exp::is_valid_value, "transform() requires f to return a valid value type."); + if (has_value()) + { + return expected(detail::exp::invoke_value_tag{}, std::forward(f)); + } + return expected(unexpect, std::move(this->m_error)); + } + /// @} + + /// If there is an error, returns an expected holding the result of invoking \p f with it; otherwise returns a + /// value. + /// @{ + template + constexpr auto transform_error(F&& f) & + { + using G = std::remove_cv_t>; + + static_assert(detail::exp::is_valid_error, "transform_error() requires f to return a valid error type."); + if (has_value()) + { + return expected(); + } + return expected(detail::exp::invoke_error_tag{}, std::forward(f), this->m_error); + } + template + constexpr auto transform_error(F&& f) const& + { + using G = std::remove_cv_t>; + + static_assert(detail::exp::is_valid_error, "transform_error() requires f to return a valid error type."); + if (has_value()) + { + return expected(); + } + return expected(detail::exp::invoke_error_tag{}, std::forward(f), this->m_error); + } + template + constexpr auto transform_error(F&& f) && + { + using G = std::remove_cv_t>; + + static_assert(detail::exp::is_valid_error, "transform_error() requires f to return a valid error type."); + if (has_value()) + { + return expected(); + } + return expected(detail::exp::invoke_error_tag{}, std::forward(f), std::move(this->m_error)); + } + template + constexpr auto transform_error(F&& f) const&& + { + using G = std::remove_cv_t>; + + static_assert(detail::exp::is_valid_error, "transform_error() requires f to return a valid error type."); + if (has_value()) + { + return expected(); + } + return expected(detail::exp::invoke_error_tag{}, std::forward(f), std::move(this->m_error)); + } + /// @} + + /// Compares with another expected with a void value type: equal if both hold a value or both hold equal errors. + /// @{ + template ::value, int> = 0> + friend constexpr detail::exp::enable_comparison() == std::declval())> + operator==(const expected& x, const expected& y) + { + return (x.has_value() == y.has_value()) && (x.has_value() || static_cast(x.error() == y.error())); + } + template ::value, int> = 0> + friend constexpr detail::exp::enable_comparison() == std::declval())> + operator!=(const expected& x, const expected& y) + { + return !(x == y); + } + /// @} + + /// Compares with an \ref unexpected. They are equal if the expected holds an error equal to the one in \p e. + /// @{ + template + friend constexpr detail::exp::enable_comparison() == std::declval())> + operator==(const expected& x, const unexpected& e) + { + return (!x.has_value()) && static_cast(x.error() == e.error()); + } + template + friend constexpr detail::exp::enable_comparison() == std::declval())> + operator==(const unexpected& e, const expected& x) + { + return (!x.has_value()) && static_cast(x.error() == e.error()); + } + template + friend constexpr detail::exp::enable_comparison() == std::declval())> + operator!=(const expected& x, const unexpected& e) + { + return !(x == e); + } + template + friend constexpr detail::exp::enable_comparison() == std::declval())> + operator!=(const unexpected& e, const expected& x) + { + return !(x == e); + } + /// @} + +private: + template + constexpr expected(const detail::exp::invoke_value_tag, F&& f, Args&&... args) + : base(detail::exp::in_place_t{}) + { + detail::exp::invoke(std::forward(f), std::forward(args)...); + } + template + constexpr expected(const detail::exp::invoke_error_tag, F&& f, Args&&... args) + : base(detail::exp::invoke_error_tag{}, std::forward(f), std::forward(args)...) + { + } + + template + void assign_error(G&& error) + { + if (has_value()) + { + detail::exp::reinit_expected(this->m_error, this->m_value, std::forward(error)); + this->m_has_value = false; + } + else + { + this->m_error = std::forward(error); + } + } +}; + +} // namespace pf23 +} // namespace cetl + +#endif // CETL_PF23_EXPECTED_HPP_INCLUDED diff --git a/include/cetl/pf26/cetlpf.hpp b/include/cetl/pf26/cetlpf.hpp index 6db4f431..e781f9e7 100644 --- a/include/cetl/pf26/cetlpf.hpp +++ b/include/cetl/pf26/cetlpf.hpp @@ -41,7 +41,7 @@ /// with said standard types where they are available. CETL polyfill types may implement a sub-set of /// the required functionality but they will not implement non-compliant functionality. #include "cetl/cetl.hpp" -#include "cetl/pf20/cetlpf.hpp" +#include "cetl/pf23/cetlpf.hpp" #include // Defines __cpp_lib_saturation_arithmetic where the standard library provides the feature. From 7f00f2951370029e7831dd70d23d6021781523ef Mon Sep 17 00:00:00 2001 From: Scott Dixon Date: Thu, 22 Oct 2026 11:19:57 -0700 Subject: [PATCH 4/7] Make expected's mutating operations constexpr from C++20 Assignment, emplace(), swap() and the constructors that convert from another expected change which member of the storage union is active. C++20 allows that in a constant expression (std::construct_at and constexpr destructors), and std::expected is constexpr throughout, so the polyfill now marks those operations constexpr when the language and library support it (__cpp_constexpr_dynamic_alloc and __cpp_lib_constexpr_dynamic_alloc). Before C++20 they cannot be constant expressions; the class documentation now says "before C++20" rather than "in C++14". A typed test runs every such operation in a constant expression, for a trivial and a non-trivial literal type, so in C++23 builds it is also checked against std::expected. A compile-failure test pins that value() && still requires a copy constructible error type: [expected.object.obs] mandates it for the rvalue overloads too, and libc++ 23 and libstdc++ 15 reject a move-only error type the same way. Co-Authored-By: Claude Opus 5.5 --- cetlvast/suites/compile/CMakeLists.txt | 1 + ..._expected_value_rvalue_move_only_error.cpp | 23 ++++ .../suites/unittest/test_pf23_expected.cpp | 89 ++++++++++++++ include/cetl/pf23/expected.hpp | 112 ++++++++++-------- 4 files changed, 178 insertions(+), 47 deletions(-) create mode 100644 cetlvast/suites/compile/test_pf23_expected_value_rvalue_move_only_error.cpp diff --git a/cetlvast/suites/compile/CMakeLists.txt b/cetlvast/suites/compile/CMakeLists.txt index ce7932b0..c79e3de4 100644 --- a/cetlvast/suites/compile/CMakeLists.txt +++ b/cetlvast/suites/compile/CMakeLists.txt @@ -15,6 +15,7 @@ set(COMPILE_TESTS ${CMAKE_CURRENT_SOURCE_DIR}/test_pf23_expected_and_then_not_expected.cpp ${CMAKE_CURRENT_SOURCE_DIR}/test_pf23_expected_transform_reference.cpp ${CMAKE_CURRENT_SOURCE_DIR}/test_pf23_expected_unexpected_value_type.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/test_pf23_expected_value_rvalue_move_only_error.cpp ${CMAKE_CURRENT_SOURCE_DIR}/test_pf26_saturating_add_bool.cpp ${CMAKE_CURRENT_SOURCE_DIR}/test_pf26_saturating_add_const_bool.cpp ${CMAKE_CURRENT_SOURCE_DIR}/test_pf26_saturating_add_mixed_types.cpp diff --git a/cetlvast/suites/compile/test_pf23_expected_value_rvalue_move_only_error.cpp b/cetlvast/suites/compile/test_pf23_expected_value_rvalue_move_only_error.cpp new file mode 100644 index 00000000..4d2f38aa --- /dev/null +++ b/cetlvast/suites/compile/test_pf23_expected_value_rvalue_move_only_error.cpp @@ -0,0 +1,23 @@ +/// @file +/// Compile test that ensures value() on an rvalue still requires a copy constructible error type, as in C++23. +/// +/// @copyright +/// Copyright (C) OpenCyphal Development Team +/// Copyright Amazon.com Inc. or its affiliates. +/// SPDX-License-Identifier: MIT +/// +#include "cetl/pf23/expected.hpp" + +#include + +int main() +{ + cetl::pf23::expected> ex{1}; +#ifndef CETLVAST_COMPILETEST_PRECHECK + // [expected.object.obs] mandates is_copy_constructible_v for value() && as well, because the error is put into + // a bad_expected_access; libc++ and libstdc++ enforce it the same way. + return std::move(ex).value() - 1; +#else + return *std::move(ex) - 1; +#endif +} diff --git a/cetlvast/suites/unittest/test_pf23_expected.cpp b/cetlvast/suites/unittest/test_pf23_expected.cpp index 225885ad..c5fcd0d2 100644 --- a/cetlvast/suites/unittest/test_pf23_expected.cpp +++ b/cetlvast/suites/unittest/test_pf23_expected.cpp @@ -757,6 +757,95 @@ TYPED_TEST(test_pf23_expected, constructor_exception_propagates) #endif // __cpp_exceptions +// +----------------------------------------------------------------------+ +// | CONSTANT EVALUATION +// +----------------------------------------------------------------------+ +#if (__cplusplus >= CETL_CPP_STANDARD_20) && defined(__cpp_constexpr_dynamic_alloc) && \ + defined(__cpp_lib_constexpr_dynamic_alloc) + +/// A literal type whose copy, move and destruction are not trivial, so that the non-trivial storage layers are used. +struct constexpr_non_trivial +{ + constexpr explicit constexpr_non_trivial(const int v) noexcept + : value{v} + { + } + constexpr constexpr_non_trivial(const constexpr_non_trivial& other) noexcept + : value{other.value} + { + } + constexpr constexpr_non_trivial(constexpr_non_trivial&& other) noexcept + : value{other.value} + { + other.value = -1; + } + constexpr constexpr_non_trivial& operator=(const constexpr_non_trivial& other) noexcept + { + value = other.value; + return *this; + } + constexpr constexpr_non_trivial& operator=(constexpr_non_trivial&& other) noexcept + { + value = other.value; + other.value = -1; + return *this; + } + constexpr ~constexpr_non_trivial() noexcept {} + + int value; +}; + +constexpr int get(const int v) +{ + return v; +} +constexpr int get(const constexpr_non_trivial& v) +{ + return v.value; +} + +/// Exercises every operation that changes the held alternative, appending one digit per step. +template +constexpr int constexpr_mutations() +{ + using ex = expected_t; + using unex = unexpected_t; + ex a(Impl::in_place(), 1); + ex b(Impl::unexpect(), 2); + a = b; // value <- error + int digits = get(a.error()); // 2 + a = T(3); // error <- value + digits = (digits * 10) + get(*a); // 23 + a = unex(T(4)); // value <- unexpected + digits = (digits * 10) + get(a.error()); // 234 + a.emplace(5); // error -> value + digits = (digits * 10) + get(*a); // 2345 + a.swap(b); // a: error 2, b: value 5 + digits = (digits * 10) + get(a.error()); // 23452 + swap(a, b); // a: value 5, b: error 2 + digits = (digits * 10) + get(*a); // 234525 + const ex converted(expected_t(6)); + digits = (digits * 10) + get(*converted); // 2345256 + expected_t v; + v = unex(T(7)); // value <- unexpected + digits = (digits * 10) + get(v.error()); // 23452567 + v.emplace(); + unex u(T(8)); + unex w(T(9)); + swap(u, w); + digits = (digits * 10) + get(u.error()); // 234525679 + return v.has_value() ? digits : -1; +} + +TYPED_TEST(test_pf23_expected, constexpr_mutations) +{ + static_assert(constexpr_mutations() == 234525679, ""); + static_assert(constexpr_mutations() == 234525679, ""); + EXPECT_EQ(234525679, (constexpr_mutations())); +} + +#endif + // +----------------------------------------------------------------------+ // | POLYFILL-SPECIFIC // +----------------------------------------------------------------------+ diff --git a/include/cetl/pf23/expected.hpp b/include/cetl/pf23/expected.hpp index 056b3512..4f0f4078 100644 --- a/include/cetl/pf23/expected.hpp +++ b/include/cetl/pf23/expected.hpp @@ -21,6 +21,16 @@ #include #include +// From C++20 a constant expression can change the active member of a union (std::construct_at and constexpr +// destructors), so the operations that do so are constexpr there, as they are in std::expected. Before C++20 they +// cannot be. The macro is undefined at the end of this header. +#if (__cplusplus >= CETL_CPP_STANDARD_20) && defined(__cpp_constexpr_dynamic_alloc) && \ + defined(__cpp_lib_constexpr_dynamic_alloc) +# define CETL_PF23_CONSTEXPR20 constexpr +#else +# define CETL_PF23_CONSTEXPR20 +#endif + namespace cetl { namespace pf23 @@ -273,13 +283,17 @@ struct invoke_error_tag final /// Constructs a union member in place. The member may be const-qualified. template -void construct_member(T& member, Args&&... args) +CETL_PF23_CONSTEXPR20 void construct_member(T& member, Args&&... args) { +#if (__cplusplus >= CETL_CPP_STANDARD_20) && defined(__cpp_lib_constexpr_dynamic_alloc) + std::construct_at(const_cast*>(std::addressof(member)), std::forward(args)...); +#else ::new (const_cast(static_cast(std::addressof(member)))) T(std::forward(args)...); +#endif } template -void destroy_member(T& member) noexcept +CETL_PF23_CONSTEXPR20 void destroy_member(T& member) noexcept { member.~T(); } @@ -297,20 +311,20 @@ using reinit_strategy = : (std::is_nothrow_move_constructible::value ? 1 : 2)>; template -void reinit(std::integral_constant, New& newval, Old& oldval, Args&&... args) +CETL_PF23_CONSTEXPR20 void reinit(std::integral_constant, New& newval, Old& oldval, Args&&... args) { detail::exp::destroy_member(oldval); detail::exp::construct_member(newval, std::forward(args)...); } template -void reinit(std::integral_constant, New& newval, Old& oldval, Args&&... args) +CETL_PF23_CONSTEXPR20 void reinit(std::integral_constant, New& newval, Old& oldval, Args&&... args) { New tmp(std::forward(args)...); detail::exp::destroy_member(oldval); detail::exp::construct_member(newval, std::move(tmp)); } template -void reinit(std::integral_constant, New& newval, Old& oldval, Args&&... args) +CETL_PF23_CONSTEXPR20 void reinit(std::integral_constant, New& newval, Old& oldval, Args&&... args) { Old tmp(std::move(oldval)); detail::exp::destroy_member(oldval); @@ -329,7 +343,7 @@ void reinit(std::integral_constant, New& newval, Old& oldval, Args&&... } template -void reinit_expected(New& newval, Old& oldval, Args&&... args) +CETL_PF23_CONSTEXPR20 void reinit_expected(New& newval, Old& oldval, Args&&... args) { detail::exp::reinit(reinit_strategy{}, newval, oldval, std::forward(args)...); } @@ -383,7 +397,7 @@ struct base_destruction { } template - explicit base_destruction(const copy_tag, Other&& other) + CETL_PF23_CONSTEXPR20 explicit base_destruction(const copy_tag, Other&& other) : m_has_value(other.m_has_value) { if (m_has_value) @@ -431,7 +445,7 @@ struct base_destruction { } template - explicit base_destruction(const copy_tag, Other&& other) + CETL_PF23_CONSTEXPR20 explicit base_destruction(const copy_tag, Other&& other) : m_has_value(other.m_has_value) { if (m_has_value) @@ -447,7 +461,7 @@ struct base_destruction constexpr base_destruction(base_destruction&&) = default; constexpr base_destruction& operator=(const base_destruction&) = default; constexpr base_destruction& operator=(base_destruction&&) = default; - ~base_destruction() noexcept + CETL_PF23_CONSTEXPR20 ~base_destruction() noexcept { if (m_has_value) { @@ -483,7 +497,7 @@ template struct base_copy_construction : base_destruction { using base_destruction::base_destruction; - base_copy_construction(const base_copy_construction& other) + CETL_PF23_CONSTEXPR20 base_copy_construction(const base_copy_construction& other) : base_destruction(copy_tag{}, other) { } @@ -522,8 +536,8 @@ struct base_move_construction : base_copy_construction { using base_copy_construction::base_copy_construction; constexpr base_move_construction(const base_move_construction&) = default; - base_move_construction(base_move_construction&& other) noexcept(std::is_nothrow_move_constructible::value && - std::is_nothrow_move_constructible::value) + CETL_PF23_CONSTEXPR20 base_move_construction(base_move_construction&& other) noexcept( + std::is_nothrow_move_constructible::value && std::is_nothrow_move_constructible::value) : base_copy_construction(copy_tag{}, std::move(other)) { } @@ -577,7 +591,7 @@ struct base_copy_assignment : base_move_construction using base_move_construction::base_move_construction; constexpr base_copy_assignment(const base_copy_assignment&) = default; constexpr base_copy_assignment(base_copy_assignment&&) = default; - base_copy_assignment& operator=(const base_copy_assignment& other) + CETL_PF23_CONSTEXPR20 base_copy_assignment& operator=(const base_copy_assignment& other) { if (this->m_has_value && other.m_has_value) { @@ -824,7 +838,7 @@ class unexpected /// @} /// Swaps the errors. - void swap(unexpected& other) noexcept(cetl::pf17::is_nothrow_swappable::value) + CETL_PF23_CONSTEXPR20 void swap(unexpected& other) noexcept(cetl::pf17::is_nothrow_swappable::value) { static_assert(cetl::pf17::is_swappable::value, "E must be swappable."); using std::swap; @@ -833,7 +847,7 @@ class unexpected template >::value, int> = 0> - friend void swap(unexpected& x, unexpected& y) noexcept(noexcept(x.swap(y))) + friend CETL_PF23_CONSTEXPR20 void swap(unexpected& x, unexpected& y) noexcept(noexcept(x.swap(y))) { x.swap(y); } @@ -866,9 +880,11 @@ unexpected(E) -> unexpected; /// Differences from \c std::expected that are visible to a program: /// - Copy and move assignment are trivial when T and E are trivially copyable, so /// \c std::is_trivially_copy_assignable> can be true where the standard leaves it unspecified. -/// - In C++14 the members that cannot be constant expressions there (assignment, emplace(), swap(), and constructors -/// that convert from another expected) are not \c constexpr, and transform() and transform_error() require the new -/// value or error type to be move constructible, because C++14 has no guaranteed copy elision. +/// - Before C++20 the members that change which alternative is held (assignment, emplace(), swap(), and constructors +/// that convert from another expected) are not \c constexpr, because only C++20 lets a constant expression change +/// the active member of a union. +/// - In C++14 transform() and transform_error() require the new value or error type to be move constructible, because +/// C++14 has no guaranteed copy elision. /// - In C++14 the in-place constructors take \c cetl::pf17::in_place_t; in C++17 and later they take /// \c std::in_place_t. \c cetl::in_place from cetl/pf17/cetlpf.hpp is the right one in every case. /// - cv-qualified void is not supported as the value type; plain void is. @@ -925,7 +941,7 @@ class expected : private detail::exp::base_move_assignment detail::exp::is_convertible, detail::exp::is_convertible>::value, int> = 0> - expected(const expected& other) // NOLINT(*-explicit-constructor) + CETL_PF23_CONSTEXPR20 expected(const expected& other) // NOLINT(*-explicit-constructor) : base(detail::exp::copy_tag{}, static_cast::base&>(other)) { } @@ -937,7 +953,7 @@ class expected : private detail::exp::base_move_assignment cetl::pf17::negation, detail::exp::is_convertible>>>::value, int> = 0> - explicit expected(const expected& other) + CETL_PF23_CONSTEXPR20 explicit expected(const expected& other) : base(detail::exp::copy_tag{}, static_cast::base&>(other)) { } @@ -947,7 +963,7 @@ class expected : private detail::exp::base_move_assignment detail::exp::is_convertible, detail::exp::is_convertible>::value, int> = 0> - expected(expected&& other) // NOLINT(*-explicit-constructor) + CETL_PF23_CONSTEXPR20 expected(expected&& other) // NOLINT(*-explicit-constructor) : base(detail::exp::copy_tag{}, static_cast::base&&>(other)) { } @@ -959,7 +975,7 @@ class expected : private detail::exp::base_move_assignment cetl::pf17::negation, detail::exp::is_convertible>>>::value, int> = 0> - explicit expected(expected&& other) + CETL_PF23_CONSTEXPR20 explicit expected(expected&& other) : base(detail::exp::copy_tag{}, static_cast::base&&>(other)) { } @@ -1077,7 +1093,7 @@ class expected : private detail::exp::base_move_assignment /// Assigns a value. template , std::enable_if_t::value, int> = 0> - expected& operator=(U&& value) + CETL_PF23_CONSTEXPR20 expected& operator=(U&& value) { if (has_value()) { @@ -1094,13 +1110,13 @@ class expected : private detail::exp::base_move_assignment /// Assigns an error. /// @{ template ::value, int> = 0> - expected& operator=(const unexpected& e) + CETL_PF23_CONSTEXPR20 expected& operator=(const unexpected& e) { assign_error(e.error()); return *this; } template ::value, int> = 0> - expected& operator=(unexpected&& e) + CETL_PF23_CONSTEXPR20 expected& operator=(unexpected&& e) { assign_error(std::move(e).error()); return *this; @@ -1110,7 +1126,7 @@ class expected : private detail::exp::base_move_assignment /// Destroys the value or the error and constructs a value in place. /// @{ template ::value, int> = 0> - T& emplace(Args&&... args) noexcept + CETL_PF23_CONSTEXPR20 T& emplace(Args&&... args) noexcept { destroy_active(); detail::exp::construct_member(this->m_value, std::forward(args)...); @@ -1120,7 +1136,7 @@ class expected : private detail::exp::base_move_assignment template &, Args...>::value, int> = 0> - T& emplace(std::initializer_list il, Args&&... args) noexcept + CETL_PF23_CONSTEXPR20 T& emplace(std::initializer_list il, Args&&... args) noexcept { destroy_active(); detail::exp::construct_member(this->m_value, il, std::forward(args)...); @@ -1133,10 +1149,10 @@ class expected : private detail::exp::base_move_assignment /// constructible is moved first so that the swap gives the strong exception guarantee. template , E>::value, int> = 0> - void swap(expected& other) noexcept(std::is_nothrow_move_constructible::value && - cetl::pf17::is_nothrow_swappable::value && - std::is_nothrow_move_constructible::value && - cetl::pf17::is_nothrow_swappable::value) + CETL_PF23_CONSTEXPR20 void swap(expected& other) noexcept(std::is_nothrow_move_constructible::value && + cetl::pf17::is_nothrow_swappable::value && + std::is_nothrow_move_constructible::value && + cetl::pf17::is_nothrow_swappable::value) { if (has_value() && other.has_value()) { @@ -1162,7 +1178,7 @@ class expected : private detail::exp::base_move_assignment template , E>::value, int> = 0> - friend void swap(expected& x, expected& y) noexcept(noexcept(x.swap(y))) + friend CETL_PF23_CONSTEXPR20 void swap(expected& x, expected& y) noexcept(noexcept(x.swap(y))) { x.swap(y); } @@ -1635,7 +1651,7 @@ class expected : private detail::exp::base_move_assignment } template - void assign_error(G&& error) + CETL_PF23_CONSTEXPR20 void assign_error(G&& error) { if (has_value()) { @@ -1648,7 +1664,7 @@ class expected : private detail::exp::base_move_assignment } } - void destroy_active() noexcept + CETL_PF23_CONSTEXPR20 void destroy_active() noexcept { if (has_value()) { @@ -1662,7 +1678,7 @@ class expected : private detail::exp::base_move_assignment /// Moves this value into other and other's error into this. E can be moved without throwing, so it is set aside /// first and put back if moving the value throws. - void swap_value_with_error(expected& other, std::true_type) + CETL_PF23_CONSTEXPR20 void swap_value_with_error(expected& other, std::true_type) { E tmp(std::move(other.m_error)); detail::exp::destroy_member(other.m_error); @@ -1682,7 +1698,7 @@ class expected : private detail::exp::base_move_assignment detail::exp::construct_member(this->m_error, std::move(tmp)); } /// As above, but only T can be moved without throwing, so the value is set aside first. - void swap_value_with_error(expected& other, std::false_type) + CETL_PF23_CONSTEXPR20 void swap_value_with_error(expected& other, std::false_type) { T tmp(std::move(this->m_value)); detail::exp::destroy_member(this->m_value); @@ -1742,7 +1758,7 @@ class expected : private detail::exp::base_move_assignment, detail::exp::is_convertible>::value, int> = 0> - expected(const expected& other) // NOLINT(*-explicit-constructor) + CETL_PF23_CONSTEXPR20 expected(const expected& other) // NOLINT(*-explicit-constructor) : base(detail::exp::copy_tag{}, static_cast::base&>(other)) { } @@ -1752,7 +1768,7 @@ class expected : private detail::exp::base_move_assignment, cetl::pf17::negation>>::value, int> = 0> - explicit expected(const expected& other) + CETL_PF23_CONSTEXPR20 explicit expected(const expected& other) : base(detail::exp::copy_tag{}, static_cast::base&>(other)) { } @@ -1761,7 +1777,7 @@ class expected : private detail::exp::base_move_assignment, detail::exp::is_convertible>::value, int> = 0> - expected(expected&& other) // NOLINT(*-explicit-constructor) + CETL_PF23_CONSTEXPR20 expected(expected&& other) // NOLINT(*-explicit-constructor) : base(detail::exp::copy_tag{}, static_cast::base&&>(other)) { } @@ -1770,7 +1786,7 @@ class expected : private detail::exp::base_move_assignment, cetl::pf17::negation>>::value, int> = 0> - explicit expected(expected&& other) + CETL_PF23_CONSTEXPR20 explicit expected(expected&& other) : base(detail::exp::copy_tag{}, static_cast::base&&>(other)) { } @@ -1853,7 +1869,7 @@ class expected : private detail::exp::base_move_assignment::value, int> = 0> - expected& operator=(const unexpected& e) + CETL_PF23_CONSTEXPR20 expected& operator=(const unexpected& e) { assign_error(e.error()); return *this; @@ -1861,7 +1877,7 @@ class expected : private detail::exp::base_move_assignment::value, int> = 0> - expected& operator=(unexpected&& e) + CETL_PF23_CONSTEXPR20 expected& operator=(unexpected&& e) { assign_error(std::move(e).error()); return *this; @@ -1869,7 +1885,7 @@ class expected : private detail::exp::base_move_assignment : private detail::exp::base_move_assignment>::value, int> = 0> - void swap(expected& other) noexcept(std::is_nothrow_move_constructible::value && - cetl::pf17::is_nothrow_swappable::value) + CETL_PF23_CONSTEXPR20 void swap(expected& other) noexcept(std::is_nothrow_move_constructible::value && + cetl::pf17::is_nothrow_swappable::value) { if (has_value() && other.has_value()) { @@ -1910,7 +1926,7 @@ class expected : private detail::exp::base_move_assignment>::value, int> = 0> - friend void swap(expected& x, expected& y) noexcept(noexcept(x.swap(y))) + friend CETL_PF23_CONSTEXPR20 void swap(expected& x, expected& y) noexcept(noexcept(x.swap(y))) { x.swap(y); } @@ -2282,7 +2298,7 @@ class expected : private detail::exp::base_move_assignment - void assign_error(G&& error) + CETL_PF23_CONSTEXPR20 void assign_error(G&& error) { if (has_value()) { @@ -2299,4 +2315,6 @@ class expected : private detail::exp::base_move_assignment Date: Thu, 22 Oct 2026 14:13:34 -0700 Subject: [PATCH 5/7] Use implicit conversions in expected comparisons and error_or The standard returns the comparison itself from the equality operators of expected and unexpected, and the argument itself from error_or, so the conversion applied is the implicit one that the constraints check. The polyfill used static_cast instead, which can pick a different, explicit conversion: a comparison result with a better-matching explicit operator bool gave the opposite answer (or did not compile if that operator was deleted), and error_or could select an explicit constructor of E over the implicit conversion from G. The comparisons now return the comparison result, as LWG 4366 (C++26) specifies for expected and as C++23 already specifies for unexpected, and error_or returns its forwarded argument. libc++ 23 behaves the same way; libstdc++ 13 and 15 still cast in the expected comparisons, so that oracle check is skipped where the cast is detected. Co-Authored-By: Claude Opus 5.5 --- .../test_pf23_expected_operations.cpp | 137 ++++++++++++++++++ include/cetl/pf23/expected.hpp | 86 +++++++++-- 2 files changed, 208 insertions(+), 15 deletions(-) diff --git a/cetlvast/suites/unittest/test_pf23_expected_operations.cpp b/cetlvast/suites/unittest/test_pf23_expected_operations.cpp index bf8ffecc..fb42a103 100644 --- a/cetlvast/suites/unittest/test_pf23_expected_operations.cpp +++ b/cetlvast/suites/unittest/test_pf23_expected_operations.cpp @@ -143,6 +143,43 @@ TYPED_TEST(test_pf23_expected_operations, error_or) EXPECT_EQ("moved", std::move(vmovable).error_or("default")); } +/// Constructible from conversion_source both explicitly (the better match) and, through the source's conversion +/// operator, implicitly; how records which one was used. +struct conversion_source; +struct conversion_target +{ + explicit conversion_target(const conversion_source&) noexcept + : how{1} + { + } + conversion_target(const int h) noexcept // NOLINT(*-explicit-constructor) + : how{h} + { + } + int how; +}; +struct conversion_source +{ + operator conversion_target() const noexcept // NOLINT(*-explicit-constructor) + { + return conversion_target{2}; + } +}; + +TYPED_TEST(test_pf23_expected_operations, error_or_converts_implicitly) +{ + // error_or returns its argument, so the implicit conversion is used even where an explicit constructor would match + // better. + using ex = expected_t; + using vex = expected_t; + const ex value(1); + const vex vvalue; + EXPECT_EQ(2, value.error_or(conversion_source{}).how); + EXPECT_EQ(2, ex(1).error_or(conversion_source{}).how); + EXPECT_EQ(2, vvalue.error_or(conversion_source{}).how); + EXPECT_EQ(2, vex().error_or(conversion_source{}).how); +} + TYPED_TEST(test_pf23_expected_operations, void_value_access) { using vex = expected_t; @@ -652,6 +689,104 @@ TEST(test_pf23_expected_cetl, invoke_member_pointers) EXPECT_EQ(5, invoke([](const int x) { return x; }, 5)); } +/// Converts to bool implicitly as true and explicitly (from a non-const object) as false, so the result of an operator +/// shows which conversion it applied. +struct conversion_witness +{ + operator bool() const noexcept // NOLINT(*-explicit-constructor) + { + return true; + } + explicit operator bool() noexcept + { + return false; + } +}; + +struct witnessed +{ + int value; +}; + +conversion_witness operator==(const witnessed&, const witnessed&) noexcept +{ + return {}; +} + +/// As conversion_witness, but the explicit conversion is deleted, so an operator that applies it does not compile. +struct deleted_explicit_bool +{ + operator bool() const noexcept // NOLINT(*-explicit-constructor) + { + return true; + } + explicit operator bool() = delete; +}; + +struct strictly_compared +{ + int value; +}; + +deleted_explicit_bool operator==(const strictly_compared&, const strictly_compared&) noexcept +{ + return {}; +} + +#if CETLVAST_HAS_STD_EXPECTED +/// True if the standard library implements LWG 4366 (libc++ 23 does; libstdc++ 13 and 15 do not). +bool std_implements_lwg4366() +{ + return std::expected(witnessed{1}) == witnessed{2}; +} +#endif + +TYPED_TEST(test_pf23_expected_operations, expected_equality_converts_implicitly) +{ + // The operators return the comparison, so its implicit conversion to bool is used (LWG 4366). +#if CETLVAST_HAS_STD_EXPECTED + if (std::is_same::value && !std_implements_lwg4366()) + { + GTEST_SKIP() << "This standard library predates LWG 4366."; + } +#endif + using wex = expected_t; + using unex = unexpected_t; + const wex value(witnessed{1}); + const wex error(TypeParam::unexpect(), witnessed{2}); + EXPECT_TRUE(value == wex(witnessed{3})); + EXPECT_TRUE(error == wex(TypeParam::unexpect(), witnessed{4})); + EXPECT_TRUE(value == witnessed{3}); + EXPECT_TRUE(witnessed{3} == value); + EXPECT_FALSE(value != witnessed{3}); + EXPECT_TRUE(error == unex(witnessed{4})); + EXPECT_TRUE(unex(witnessed{4}) == error); + using vex = expected_t; + const vex verror(TypeParam::unexpect(), witnessed{2}); + EXPECT_TRUE(verror == vex(TypeParam::unexpect(), witnessed{4})); + EXPECT_TRUE(verror == unex(witnessed{4})); +} + +TEST(test_pf23_expected_cetl, equality_never_converts_explicitly) +{ + // With the explicit conversion deleted, these compile only because no operator applies it (LWG 4366). + using sex = cetl::pf23::expected; + using unex = cetl::pf23::unexpected; + using vex = cetl::pf23::expected; + const sex value(strictly_compared{1}); + const sex error(cetl::pf23::unexpect, strictly_compared{2}); + const vex verror(cetl::pf23::unexpect, strictly_compared{3}); + EXPECT_TRUE(value == sex(strictly_compared{4})); + EXPECT_TRUE(error == sex(cetl::pf23::unexpect, strictly_compared{4})); + EXPECT_TRUE(value == strictly_compared{4}); + EXPECT_TRUE(strictly_compared{4} == value); + EXPECT_TRUE(error == unex(strictly_compared{4})); + EXPECT_TRUE(unex(strictly_compared{4}) == error); + EXPECT_TRUE(verror == vex(cetl::pf23::unexpect, strictly_compared{4})); + EXPECT_TRUE(verror == unex(strictly_compared{4})); + EXPECT_TRUE(unex(strictly_compared{1}) == unex(strictly_compared{4})); +} + // +----------------------------------------------------------------------+ // | UNEXPECTED // +----------------------------------------------------------------------+ @@ -681,6 +816,8 @@ TYPED_TEST(test_pf23_expected_operations, unexpected) swap(copy, from_value); EXPECT_EQ(5, copy.error().value); EXPECT_TRUE((unexpected_t(1) == unexpected_t(1L))); + // The comparison is returned, so its implicit conversion to bool is used. + EXPECT_TRUE((unexpected_t(witnessed{1}) == unexpected_t(witnessed{2}))); // Construction from a value is explicit. static_assert(!std::is_convertible>::value, ""); static_assert(std::is_constructible, int>::value, ""); diff --git a/include/cetl/pf23/expected.hpp b/include/cetl/pf23/expected.hpp index 4f0f4078..a928779e 100644 --- a/include/cetl/pf23/expected.hpp +++ b/include/cetl/pf23/expected.hpp @@ -767,7 +767,8 @@ template using enable_void_swap = cetl::pf17::conjunction, std::is_move_constructible>; /// The equality operators take part in overload resolution only if every comparison they make is well-formed and -/// yields something convertible to bool (P3379R0). +/// yields something implicitly convertible to bool (P3379R0). They return the result of the comparison, so that +/// conversion is the one used, never an explicit one (LWG 4366). template using enable_comparison = std::enable_if_t...>::value, bool>; @@ -857,7 +858,7 @@ class unexpected template friend constexpr bool operator==(const unexpected& x, const unexpected& y) { - return static_cast(x.error() == y.error()); + return x.error() == y.error(); } template friend constexpr bool operator!=(const unexpected& x, const unexpected& y) @@ -1324,14 +1325,22 @@ class expected : private detail::exp::base_move_assignment { static_assert(std::is_copy_constructible::value, "error_or() requires a copy constructible error type."); static_assert(detail::exp::is_convertible::value, "error_or() requires an argument convertible to E."); - return has_value() ? static_cast(std::forward(default_error)) : this->m_error; + if (has_value()) + { + return std::forward(default_error); + } + return this->m_error; } template CETL_NODISCARD constexpr E error_or(G&& default_error) && { static_assert(std::is_move_constructible::value, "error_or() requires a move constructible error type."); static_assert(detail::exp::is_convertible::value, "error_or() requires an argument convertible to E."); - return has_value() ? static_cast(std::forward(default_error)) : std::move(this->m_error); + if (has_value()) + { + return std::forward(default_error); + } + return std::move(this->m_error); } /// @} @@ -1570,8 +1579,15 @@ class expected : private detail::exp::base_move_assignment decltype(std::declval() == std::declval())> operator==(const expected& x, const expected& y) { - return (x.has_value() == y.has_value()) && - (x.has_value() ? static_cast(*x == *y) : static_cast(x.error() == y.error())); + if (x.has_value() != y.has_value()) + { + return false; + } + if (x.has_value()) + { + return *x == *y; + } + return x.error() == y.error(); } template ::value, int> = 0> friend constexpr detail::exp::enable_comparison() == std::declval()), @@ -1588,13 +1604,21 @@ class expected : private detail::exp::base_move_assignment friend constexpr detail::exp::enable_comparison() == std::declval())> operator==(const expected& x, const T2& v) { - return x.has_value() && static_cast(*x == v); + if (x.has_value()) + { + return *x == v; + } + return false; } template ::value, int> = 0> friend constexpr detail::exp::enable_comparison() == std::declval())> operator==(const T2& v, const expected& x) { - return x.has_value() && static_cast(*x == v); + if (x.has_value()) + { + return *x == v; + } + return false; } template ::value, int> = 0> friend constexpr detail::exp::enable_comparison() == std::declval())> @@ -1616,13 +1640,21 @@ class expected : private detail::exp::base_move_assignment friend constexpr detail::exp::enable_comparison() == std::declval())> operator==(const expected& x, const unexpected& e) { - return (!x.has_value()) && static_cast(x.error() == e.error()); + if (!x.has_value()) + { + return x.error() == e.error(); + } + return false; } template friend constexpr detail::exp::enable_comparison() == std::declval())> operator==(const unexpected& e, const expected& x) { - return (!x.has_value()) && static_cast(x.error() == e.error()); + if (!x.has_value()) + { + return x.error() == e.error(); + } + return false; } template friend constexpr detail::exp::enable_comparison() == std::declval())> @@ -2005,14 +2037,22 @@ class expected : private detail::exp::base_move_assignment::value, "error_or() requires a copy constructible error type."); static_assert(detail::exp::is_convertible::value, "error_or() requires an argument convertible to E."); - return has_value() ? static_cast(std::forward(default_error)) : this->m_error; + if (has_value()) + { + return std::forward(default_error); + } + return this->m_error; } template CETL_NODISCARD constexpr E error_or(G&& default_error) && { static_assert(std::is_move_constructible::value, "error_or() requires a move constructible error type."); static_assert(detail::exp::is_convertible::value, "error_or() requires an argument convertible to E."); - return has_value() ? static_cast(std::forward(default_error)) : std::move(this->m_error); + if (has_value()) + { + return std::forward(default_error); + } + return std::move(this->m_error); } /// @} @@ -2246,7 +2286,15 @@ class expected : private detail::exp::base_move_assignment() == std::declval())> operator==(const expected& x, const expected& y) { - return (x.has_value() == y.has_value()) && (x.has_value() || static_cast(x.error() == y.error())); + if (x.has_value() != y.has_value()) + { + return false; + } + if (x.has_value()) + { + return true; + } + return x.error() == y.error(); } template ::value, int> = 0> friend constexpr detail::exp::enable_comparison() == std::declval())> @@ -2262,13 +2310,21 @@ class expected : private detail::exp::base_move_assignment() == std::declval())> operator==(const expected& x, const unexpected& e) { - return (!x.has_value()) && static_cast(x.error() == e.error()); + if (!x.has_value()) + { + return x.error() == e.error(); + } + return false; } template friend constexpr detail::exp::enable_comparison() == std::declval())> operator==(const unexpected& e, const expected& x) { - return (!x.has_value()) && static_cast(x.error() == e.error()); + if (!x.has_value()) + { + return x.error() == e.error(); + } + return false; } template friend constexpr detail::exp::enable_comparison() == std::declval())> From 793f7b3d4ac076cb00b65ad6e5ab6146ac223dd7 Mon Sep 17 00:00:00 2001 From: Scott Dixon Date: Thu, 22 Oct 2026 14:24:53 -0700 Subject: [PATCH 6/7] Test unexpected swap where the standard libraries agree [expected.un.swap] mandates is_swappable_v for the member swap and constrains the friend swap on it. libc++ follows that wording; libstdc++ constrains the member swap as well, so code that only asks whether x.swap(y) is callable gets different answers. The polyfill keeps the standard's wording. A typed test now checks, against libc++ and libstdc++ as well as the polyfill, the parts they agree on: the friend swap is constrained, so the swappable traits follow E (including nothrow), and swapping works for swappable error types. A compile-failure test checks that calling the member swap with a non-swappable error does not compile, which holds under either reading. Co-Authored-By: Claude Opus 5.5 --- cetlvast/suites/compile/CMakeLists.txt | 1 + ...est_pf23_unexpected_swap_not_swappable.cpp | 39 +++++++++++++++++++ .../test_pf23_expected_operations.cpp | 33 ++++++++++++++++ 3 files changed, 73 insertions(+) create mode 100644 cetlvast/suites/compile/test_pf23_unexpected_swap_not_swappable.cpp diff --git a/cetlvast/suites/compile/CMakeLists.txt b/cetlvast/suites/compile/CMakeLists.txt index c79e3de4..cf8adb32 100644 --- a/cetlvast/suites/compile/CMakeLists.txt +++ b/cetlvast/suites/compile/CMakeLists.txt @@ -16,6 +16,7 @@ set(COMPILE_TESTS ${CMAKE_CURRENT_SOURCE_DIR}/test_pf23_expected_transform_reference.cpp ${CMAKE_CURRENT_SOURCE_DIR}/test_pf23_expected_unexpected_value_type.cpp ${CMAKE_CURRENT_SOURCE_DIR}/test_pf23_expected_value_rvalue_move_only_error.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/test_pf23_unexpected_swap_not_swappable.cpp ${CMAKE_CURRENT_SOURCE_DIR}/test_pf26_saturating_add_bool.cpp ${CMAKE_CURRENT_SOURCE_DIR}/test_pf26_saturating_add_const_bool.cpp ${CMAKE_CURRENT_SOURCE_DIR}/test_pf26_saturating_add_mixed_types.cpp diff --git a/cetlvast/suites/compile/test_pf23_unexpected_swap_not_swappable.cpp b/cetlvast/suites/compile/test_pf23_unexpected_swap_not_swappable.cpp new file mode 100644 index 00000000..d5a0e83c --- /dev/null +++ b/cetlvast/suites/compile/test_pf23_unexpected_swap_not_swappable.cpp @@ -0,0 +1,39 @@ +/// @file +/// Compile test that ensures unexpected::swap() cannot be called with an error type that is not swappable. +/// +/// [expected.un.swap] makes is_swappable_v a Mandates of the member swap, which this polyfill and libc++ enforce +/// with a static_assert; libstdc++ makes it a constraint instead. Either way the call does not compile. +/// +/// @copyright +/// Copyright (C) OpenCyphal Development Team +/// Copyright Amazon.com Inc. or its affiliates. +/// SPDX-License-Identifier: MIT +/// +#include "cetl/pf23/expected.hpp" + +namespace +{ +struct pinned +{ + explicit pinned(const int v) noexcept + : value{v} + { + } + pinned(const pinned&) = delete; + pinned(pinned&&) = delete; + pinned& operator=(const pinned&) = delete; + pinned& operator=(pinned&&) = delete; + ~pinned() = default; + int value; +}; +} // namespace + +int main() +{ + cetl::pf23::unexpected a{1}; + cetl::pf23::unexpected b{2}; +#ifndef CETLVAST_COMPILETEST_PRECHECK + a.swap(b); +#endif + return a.error().value + b.error().value - 3; +} diff --git a/cetlvast/suites/unittest/test_pf23_expected_operations.cpp b/cetlvast/suites/unittest/test_pf23_expected_operations.cpp index fb42a103..9700a2ca 100644 --- a/cetlvast/suites/unittest/test_pf23_expected_operations.cpp +++ b/cetlvast/suites/unittest/test_pf23_expected_operations.cpp @@ -824,6 +824,39 @@ TYPED_TEST(test_pf23_expected_operations, unexpected) static_assert(!std::is_default_constructible>::value, ""); } +TYPED_TEST(test_pf23_expected_operations, unexpected_swap_constraints) +{ + // What libc++ and libstdc++ agree on ([expected.un.swap]): the free swap takes part in overload resolution only if + // E is swappable, so the swappable traits see through to E, and swapping works when E is swappable. They disagree + // on whether the member swap is constrained (libstdc++) or only mandated (the standard's wording, libc++ and this + // polyfill), so that is not checked here; test_pf23_unexpected_swap_not_swappable.cpp checks that calling it with + // a non-swappable E does not compile. + static_assert(cetl::pf17::is_swappable>::value, ""); + static_assert(cetl::pf17::is_nothrow_swappable>::value, ""); + static_assert(cetl::pf17::is_swappable>::value, ""); + static_assert(!cetl::pf17::is_swappable>::value, ""); + static_assert(cetl::pf17::is_swappable>::value, ""); + static_assert(!cetl::pf17::is_nothrow_swappable>::value, ""); +#if __cplusplus >= CETL_CPP_STANDARD_17 + static_assert(std::is_swappable_v>, ""); + static_assert(!std::is_swappable_v>, ""); + static_assert(!std::is_nothrow_swappable_v>, ""); +#endif + unexpected_t a(move_only{1}); + unexpected_t b(move_only{2}); + swap(a, b); + EXPECT_EQ(2, a.error().value); + EXPECT_EQ(1, b.error().value); + a.swap(b); + EXPECT_EQ(1, a.error().value); + EXPECT_EQ(2, b.error().value); + unexpected_t c(potentially_throwing_int{3}); + unexpected_t d(potentially_throwing_int{4}); + swap(c, d); + EXPECT_EQ(4, c.error().value); + EXPECT_EQ(3, d.error().value); +} + #if defined(__cpp_deduction_guides) TEST(test_pf23_expected_cetl, unexpected_deduction_guide) { From 7eb5d28905e01d85877d4ac48aae909c5eace5ac Mon Sep 17 00:00:00 2001 From: Scott Dixon Date: Sat, 24 Oct 2026 21:00:08 -0700 Subject: [PATCH 7/7] Add the cpp-26 amd64 jobs to the release contract The release tooling's required_jobs() must list every CETLVaSt matrix entry, and its contract test fails when the workflow matrix and the list disagree. The cpp-26 standard added to the amd64 matrix was missing from the list, which failed release-tests and therefore release-ready on #255. Co-Authored-By: Claude Fable 5.1 --- scripts/release_tools.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/release_tools.py b/scripts/release_tools.py index cc7f7f96..74c88c9a 100644 --- a/scripts/release_tools.py +++ b/scripts/release_tools.py @@ -307,7 +307,7 @@ def required_jobs(): """ names = {"version", "release-tests", "cache-warmup", "docs", "release-ready", "release-assets"} for architecture, compilers, standards in ( - ("amd64", ("gcc-native-32", "clang-native"), (14, 17, 20, 23)), + ("amd64", ("gcc-native-32", "clang-native"), (14, 17, 20, 23, 26)), ("arm64", ("gcc-native", "clang-native"), (14, 17, 20)), ("legacy-arm64", ("gcc7-native",), (14,)), ):