The change seems unsafe because none of the other functions currently calling value() are switched to use operator* instead, so they will now throw instead of assert if the value is not set. This would make it dangerous to swap the util::Expected class with std::expected in the future.
I understand that switching util::Expected::operator* to std::expected::operator* is unsafe, but this is documented in https://en.cppreference.com/w/cpp/utility/expected/operator%252A.html:
If has_value() is false, the behavior is undefined. (until C++26)
Though, that is true with or without this pull request, so it seems unrelated.
As for this implementation: I think it is perfectly fine to do whatever on UB, and asserting, or throwing seems better than UB.
but asserts would probably lead to clearer runtime errors than noexcept violations.
I don’t understand why that would be. The only difference with an assert is that it prints the file name and line number, but this will just be a inside this util header, not in the place in the code where the assertion happens. So reading the tb is needed anyway. For reference, with noexecpt, it looks like:
0valgrind --tool=none ./bld-cmake/bin/test_bitcoin -t util_expected_tests/expected_error --catch_system_errors=no
1==1207793== Nulgrind, the minimal Valgrind tool
2...
3==1207793== Command: ./bld-cmake/bin/test_bitcoin -t util_expected_tests/expected_error --catch_system_errors=no
4==1207793==
5Running 1 test case...
6terminate called after throwing an instance of 'util::BadExpectedAccess'
7 what(): Bad util::Expected access
8==1207793==
9==1207793== Process terminating with default action of signal 6 (SIGABRT): dumping core
10...
11==1207793== by 0x4F1A98A: util_expected_tests::expected_error::test_method() (expected.h:117)
12==1207793== by 0x4F19B56: util_expected_tests::expected_error_invoker() (./test/util_expected_tests.cpp:79)
13...
14Aborted (core dumped)
I think this is perfectly fine, and I don’t see the benefit of the assertion.