Skip to content

Commit ada08fc

Browse files
committed
st: fix CI - Expected analyzer false positive via std::get_if
clang-analyzer (LLVM 18 + libstdc++) cannot model the std::variant discriminant through std::get<1>, so it treats the Error returned by Expected::error() as having an uninitialized `result` field and flags every Error copy/move that originates from it (reported at Result.h:107, the Error copy/move ctor). std::get_if<1> - already used without complaint by operator-> - returns a pointer the analyzer models as defined, so error() now reads through it, and value() routes through error() instead of raw std::get<1>. get_if also better fits the noexcept accessor (std::get can throw bad_variant_access). Revert the ineffective `Result result{}` default-init in the classic Result.h header (the analyzer flags the copy, not default construction; the real fix is above), restoring that header to pristine. Suppress an unrelated NewDeleteLeaks false positive on a move-only mapper in FutureTest.
1 parent c11e919 commit ada08fc

3 files changed

Lines changed: 11 additions & 6 deletions

File tree

include/pulsar/Result.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,7 @@ PULSAR_PUBLIC const char* strResult(Result result);
105105
PULSAR_PUBLIC std::ostream& operator<<(std::ostream& s, pulsar::Result result);
106106

107107
struct PULSAR_PUBLIC Error {
108-
Result result{}; // defaults to ResultOk; avoids an uninitialized field on default construction
108+
Result result;
109109
std::string message;
110110
};
111111

include/pulsar/st/Expected.h

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -124,15 +124,15 @@ class [[nodiscard]] Expected {
124124
* this aborts. Use `operator bool` + `operator*` for the non-throwing path.
125125
*/
126126
const T& value() const& {
127-
if (!has_value()) PULSAR_ST_THROW(ClientException(std::get<1>(storage_)));
127+
if (!has_value()) PULSAR_ST_THROW(ClientException(error()));
128128
return std::get<0>(storage_);
129129
}
130130
T& value() & {
131-
if (!has_value()) PULSAR_ST_THROW(ClientException(std::get<1>(storage_)));
131+
if (!has_value()) PULSAR_ST_THROW(ClientException(error()));
132132
return std::get<0>(storage_);
133133
}
134134
T&& value() && {
135-
if (!has_value()) PULSAR_ST_THROW(ClientException(std::get<1>(storage_)));
135+
if (!has_value()) PULSAR_ST_THROW(ClientException(error()));
136136
return std::get<0>(std::move(storage_));
137137
}
138138

@@ -170,9 +170,9 @@ class [[nodiscard]] Expected {
170170
* @pre `!has_value()`. Behaviour is undefined if this holds a value.
171171
* @return a reference to the contained `Error`.
172172
*/
173-
const Error& error() const& noexcept { return std::get<1>(storage_); }
173+
const Error& error() const& noexcept { return *std::get_if<1>(&storage_); }
174174
/** @copydoc error() const& */
175-
Error& error() & noexcept { return std::get<1>(storage_); }
175+
Error& error() & noexcept { return *std::get_if<1>(&storage_); }
176176

177177
/**
178178
* Return the contained value, or @p fallback if this holds an error.

tests/st/FutureTest.cc

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,8 +131,13 @@ TEST(FutureTest, testThenApplyVoidMapper) {
131131
TEST(FutureTest, testThenApplyMoveOnlyMapper) {
132132
Promise<int> promise;
133133
auto bonus = std::make_unique<int>(100);
134+
// The unique_ptr is moved into the mapper, which the Future's shared state owns and
135+
// frees when the chain is destroyed; the analyzer can't trace that through
136+
// std::function, so it false-positives a leak.
137+
// NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks)
134138
Future<int> mapped =
135139
promise.getFuture().thenApply([b = std::move(bonus)](const int& x) { return x + *b; });
140+
// NOLINTEND(clang-analyzer-cplusplus.NewDeleteLeaks)
136141
promise.setValue(5);
137142
auto r = mapped.get();
138143
ASSERT_TRUE(r);

0 commit comments

Comments
 (0)