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..cf8adb32 100644 --- a/cetlvast/suites/compile/CMakeLists.txt +++ b/cetlvast/suites/compile/CMakeLists.txt @@ -11,6 +11,15 @@ 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_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 ${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_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/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/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/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_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/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..82a4b8a8 100644 --- a/cetlvast/suites/docs/examples/CMakeLists.txt +++ b/cetlvast/suites/docs/examples/CMakeLists.txt @@ -33,6 +33,8 @@ 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 + example_14_expected.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/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/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 00000000..c80321b8 Binary files /dev/null and b/cetlvast/suites/fuzz/corpus/saturation_arithmetic/int32_min_div_neg1 differ 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 00000000..5d8d55ea Binary files /dev/null and b/cetlvast/suites/fuzz/corpus/saturation_arithmetic/int64_min_times_neg1 differ 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 00000000..9c58e7dd Binary files /dev/null and b/cetlvast/suites/fuzz/corpus/saturation_arithmetic/int8_min_minus_one differ 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 00000000..a4241b8d Binary files /dev/null and b/cetlvast/suites/fuzz/corpus/saturation_arithmetic/uint64_max_times_two differ 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 00000000..66455be8 Binary files /dev/null and b/cetlvast/suites/fuzz/corpus/saturation_arithmetic/uint8_max_plus_one differ 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..daf9c9c2 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,11 @@ set(NATIVE_TESTS test_pf20_span_asserts.cpp 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 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..8c36b0b8 --- /dev/null +++ b/cetlvast/suites/unittest/test_pf20_utility.cpp @@ -0,0 +1,236 @@ +/// @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"); + +// 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"); + +// +----------------------------------------------------------------------+ +// | 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_pf23_expected.cpp b/cetlvast/suites/unittest/test_pf23_expected.cpp new file mode 100644 index 00000000..c5fcd0d2 --- /dev/null +++ b/cetlvast/suites/unittest/test_pf23_expected.cpp @@ -0,0 +1,893 @@ +/// @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 + +// +----------------------------------------------------------------------+ +// | 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 +// +----------------------------------------------------------------------+ + +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..9700a2ca --- /dev/null +++ b/cetlvast/suites/unittest/test_pf23_expected_operations.cpp @@ -0,0 +1,893 @@ +/// @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")); +} + +/// 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; + 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)); +} + +/// 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 +// +----------------------------------------------------------------------+ + +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))); + // 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, ""); + 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) +{ + 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/cetlvast/suites/unittest/test_pf26_numeric.cpp b/cetlvast/suites/unittest/test_pf26_numeric.cpp new file mode 100644 index 00000000..2ce0a382 --- /dev/null +++ b/cetlvast/suites/unittest/test_pf26_numeric.cpp @@ -0,0 +1,530 @@ +/// @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"); + +// 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, ""); +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..62d8be22 100644 --- a/include/cetl/cetl.hpp +++ b/include/cetl/cetl.hpp @@ -33,6 +33,8 @@ /// - @subpage example_10_unbounded_variant /// - @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 @@ -83,6 +85,14 @@ /// 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 +/// +/// @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 @@ -195,6 +205,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..6d17db86 --- /dev/null +++ b/include/cetl/pf20/utility.hpp @@ -0,0 +1,249 @@ +/// @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 +{ +// +----------------------------------------------------------------------------------------------------------------+ +// | 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 +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> +{}; +#if defined(__cpp_char8_t) +template <> +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 == 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 +{ + 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 +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 +/// 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::value, + "in_range requires standard or extended integer types (not bool or character types)."); + return cmp_greater_equal(t, detail::integer_limits>::min()) && + cmp_less_equal(t, detail::integer_limits>::max()); +} + +} // namespace pf20 +} // namespace cetl + +#endif // CETL_PF20_UTILITY_HPP_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..a928779e --- /dev/null +++ b/include/cetl/pf23/expected.hpp @@ -0,0 +1,2376 @@ +/// @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 + +// 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 +{ +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 +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 +CETL_PF23_CONSTEXPR20 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 +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 +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 +CETL_PF23_CONSTEXPR20 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 +CETL_PF23_CONSTEXPR20 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 + CETL_PF23_CONSTEXPR20 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 + CETL_PF23_CONSTEXPR20 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; + CETL_PF23_CONSTEXPR20 ~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; + CETL_PF23_CONSTEXPR20 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; + 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)) + { + } + 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; + CETL_PF23_CONSTEXPR20 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 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>; + +} // 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. + 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; + swap(m_error, other.m_error); + } + + template >::value, int> = 0> + friend CETL_PF23_CONSTEXPR20 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 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. +/// - 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. +/// - 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> + CETL_PF23_CONSTEXPR20 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> + CETL_PF23_CONSTEXPR20 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> + CETL_PF23_CONSTEXPR20 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> + CETL_PF23_CONSTEXPR20 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> + CETL_PF23_CONSTEXPR20 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> + CETL_PF23_CONSTEXPR20 expected& operator=(const unexpected& e) + { + assign_error(e.error()); + return *this; + } + template ::value, int> = 0> + CETL_PF23_CONSTEXPR20 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> + CETL_PF23_CONSTEXPR20 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> + 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)...); + 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> + 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()) + { + 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 CETL_PF23_CONSTEXPR20 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."); + 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."); + if (has_value()) + { + return std::forward(default_error); + } + return 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) + { + 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()), + 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) + { + 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) + { + if (x.has_value()) + { + return *x == v; + } + return false; + } + 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) + { + 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) + { + if (!x.has_value()) + { + return x.error() == e.error(); + } + return false; + } + 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 + CETL_PF23_CONSTEXPR20 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); + } + } + + CETL_PF23_CONSTEXPR20 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. + 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); +#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. + 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); +#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> + CETL_PF23_CONSTEXPR20 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> + CETL_PF23_CONSTEXPR20 explicit expected(const expected& other) + : base(detail::exp::copy_tag{}, static_cast::base&>(other)) + { + } + template , + detail::exp::is_convertible>::value, + int> = 0> + CETL_PF23_CONSTEXPR20 expected(expected&& other) // NOLINT(*-explicit-constructor) + : base(detail::exp::copy_tag{}, static_cast::base&&>(other)) + { + } + template , + cetl::pf17::negation>>::value, + int> = 0> + CETL_PF23_CONSTEXPR20 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> + CETL_PF23_CONSTEXPR20 expected& operator=(const unexpected& e) + { + assign_error(e.error()); + return *this; + } + template < + typename G, + std::enable_if_t::value, int> = 0> + CETL_PF23_CONSTEXPR20 expected& operator=(unexpected&& e) + { + assign_error(std::move(e).error()); + return *this; + } + /// @} + + /// Destroys the error, if any, so that the expected holds a value. + CETL_PF23_CONSTEXPR20 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> + 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()) + { + // 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 CETL_PF23_CONSTEXPR20 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."); + 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."); + if (has_value()) + { + return std::forward(default_error); + } + return 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) + { + 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())> + 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) + { + 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) + { + if (!x.has_value()) + { + return x.error() == e.error(); + } + return false; + } + 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 + CETL_PF23_CONSTEXPR20 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 + +#undef CETL_PF23_CONSTEXPR20 + +#endif // CETL_PF23_EXPECTED_HPP_INCLUDED diff --git a/include/cetl/pf26/cetlpf.hpp b/include/cetl/pf26/cetlpf.hpp new file mode 100644 index 00000000..e781f9e7 --- /dev/null +++ b/include/cetl/pf26/cetlpf.hpp @@ -0,0 +1,107 @@ +/// @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/pf23/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 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 auto saturating_add(T x, T y) noexcept -> decltype(std::add_sat(x, y)) +{ + return std::add_sat(x, y); +} +template +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 auto saturating_mul(T x, T y) noexcept -> decltype(std::mul_sat(x, y)) +{ + return std::mul_sat(x, y); +} +template +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 auto saturating_cast(U x) noexcept -> decltype(std::saturate_cast(x)) +{ + 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..9cc38551 --- /dev/null +++ b/include/cetl/pf26/numeric.hpp @@ -0,0 +1,253 @@ +/// @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 +/// (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>; + +// +----------------------------------------------------------------------------------------------------------------+ +// | 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) ? limits::min() : limits::max(); +} +template +constexpr T add_saturated(T, std::false_type /*unsigned*/) noexcept +{ + return limits::max(); +} +template +constexpr T sub_saturated(T x, std::true_type /*signed*/) noexcept +{ + return (x < 0) ? limits::min() : limits::max(); +} +template +constexpr T sub_saturated(T, std::false_type /*unsigned*/) noexcept +{ + return limits::min(); +} +template +constexpr T mul_saturated(T x, T y, std::true_type /*signed*/) noexcept +{ + return ((x < 0) != (y < 0)) ? limits::min() : limits::max(); +} +template +constexpr T mul_saturated(T, T, std::false_type /*unsigned*/) noexcept +{ + return 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(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(limits::max() - y); +} +template +constexpr bool sub_overflows(T x, T y, std::true_type /*signed*/) noexcept +{ + 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 +{ + 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 > 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 > limits::max() / y); +} +template +constexpr bool is_min_over_minus_one(T x, T y, std::true_type /*signed*/) noexcept +{ + return (x == 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, 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, 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, is_signed{}) ? mul_saturated(x, y, 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, 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, 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, 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_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. +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, detail::is_signed{}) ? detail::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, detail::limits::min()) ? detail::limits::min() + : pf20::cmp_greater(x, detail::limits::max()) ? detail::limits::max() + : static_cast(x); +} + +} // namespace pf26 +} // namespace cetl + +#endif // CETL_PF26_NUMERIC_HPP_INCLUDED 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,)), ):