Problem
`Response` bundles success state and error in one struct, and callers signal "did it work?" with `explicit operator bool()` (see `include/miniocpp/response.h:32-71`). It works but:
- Forgetting to check `if (!resp)` is silent — you can read `resp.etag` on a failed response and get an empty string
- Every operation duplicates the bool-conversion gate
- Error and success paths share memory in every response struct
Suggested approach
```cpp
std::expected<PutObjectResponse, error::Error> PutObject(PutObjectArgs args);
```
Now `auto r = client.PutObject(args)` either holds the response or the error, never both. `r.has_value()` / `*r` for the happy path; `r.error()` otherwise. Compiler catches "you didn't check" via `[[nodiscard]]`.
Constraints
Impact
Large. Single biggest API quality win — shifts error handling from runtime convention to compile-time type-enforcement.
Roadmap
Tier 3 item from the C++ modernization audit. Highest-leverage of the modernization items but also the biggest API surface change.
Problem
`Response` bundles success state and error in one struct, and callers signal "did it work?" with `explicit operator bool()` (see `include/miniocpp/response.h:32-71`). It works but:
Suggested approach
```cpp
std::expected<PutObjectResponse, error::Error> PutObject(PutObjectArgs args);
```
Now `auto r = client.PutObject(args)` either holds the response or the error, never both. `r.has_value()` / `*r` for the happy path; `r.error()` otherwise. Compiler catches "you didn't check" via `[[nodiscard]]`.
Constraints
Impact
Large. Single biggest API quality win — shifts error handling from runtime convention to compile-time type-enforcement.
Roadmap
Tier 3 item from the C++ modernization audit. Highest-leverage of the modernization items but also the biggest API surface change.