CppInterOp
C++ Language Interoperability Layer
Loading...
Searching...
No Matches
Error.h
Go to the documentation of this file.
1//===--- Error.h - Cpp::Result<T> -------------------------------*- C++ -*-===//
2//
3// Part of the compiler-research project, under the Apache License v2.0 with
4// LLVM Exceptions.
5// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
6//
7//===----------------------------------------------------------------------===//
8//
9// Cpp::Result<T> -- the [[nodiscard]] return type fallible APIs use.
10// A Result holds either a value of T or an ErrorRef. The discipline
11// matches llvm::Expected: call .ok() (or coerce to bool), then .value()
12// on success or .error() on failure. value() on an error aborts;
13// value_or(fallback) is the lenient form.
14//
15// In debug builds, dropping an error-bearing Result without inspecting
16// it aborts. .ignore() opts out of that check.
17//
18// This PR is just the type. Later PRs add the boundary translator,
19// per-call diagnostics, and structured failure payloads.
20//
21//===----------------------------------------------------------------------===//
22
23#ifndef CPPINTEROP_ERROR_H
24#define CPPINTEROP_ERROR_H
25
27
28#ifdef __cplusplus
29
30#include <cstddef>
31#include <cstdint>
32#include <new>
33#include <type_traits>
34#include <utility>
35
36namespace Cpp {
37
38/// Coarse outcome code. Status::Failed is a placeholder -- later PRs
39/// refine the taxonomy (NotFound, ParseError, ...) as APIs migrate.
40enum class Status : uint8_t {
41 Ok = 0,
42 Failed,
43};
44
45/// 8-byte opaque error handle. nullptr state means "no error". A
46/// non-null state is an opaque pointer interpreted by later PRs.
47struct ErrorRef {
48 const void* state = nullptr;
49
50 [[nodiscard]] bool isOk() const { return state == nullptr; }
51};
52
53/// Out-of-line abort for value()-on-error. Keeps Result<T>'s inline
54/// body small.
55[[noreturn]] CPPINTEROP_API void ResultAbort_ValueOnError(const ErrorRef& Err);
56
57#ifndef NDEBUG
58/// Out-of-line abort for the debug must-handle check. Shared between
59/// Result<T> and Result<void> so the message and abort path live in
60/// one place.
61[[noreturn]] CPPINTEROP_API void
62ResultAbort_UncheckedOnDtor(const ErrorRef& Err);
63#endif
64
65// Result<T>: union of T storage and the ErrorRef pointer, plus a
66// HasError discriminator. Sized comparably to llvm::Expected<T>
67// (pinned by static_assert in ResultBench). Debug adds an Unchecked
68// bit; release shrinks to bare union + discriminator.
69
70template <typename T> class [[nodiscard]] Result {
71 static_assert(!std::is_same<T, void>::value,
72 "Result<void> is provided via specialization below");
73
74 static constexpr size_t kStorageSize = sizeof(T) > sizeof(void*)
75 ? sizeof(T)
76 : sizeof(void*);
77 static constexpr size_t kStorageAlign = alignof(T) > alignof(void*)
78 ? alignof(T)
79 : alignof(void*);
80
81 union {
82 alignas(kStorageAlign) unsigned char m_ValueBytes[kStorageSize];
83 const void* m_ErrState; // when m_HasError == true
84 };
85 bool m_HasError = false;
86
87#ifndef NDEBUG
88 mutable bool m_Unchecked = true;
89 void markChecked() const { m_Unchecked = false; }
90#else
91 void markChecked() const {}
92#endif
93
94 // reinterpret_cast across the union member -- std::launder is C++17
95 // and the header has to parse in C++14 consumer contexts.
96 T* valuePtr() { return reinterpret_cast<T*>(m_ValueBytes); }
97 const T* valuePtr() const { return reinterpret_cast<const T*>(m_ValueBytes); }
98
99public:
100 Result() { new (m_ValueBytes) T(); }
101
102 Result(T V) { new (m_ValueBytes) T(std::move(V)); }
103
104 Result(ErrorRef E) : m_ErrState(E.state), m_HasError(true) {}
105
106 Result(const Result& Other) : m_HasError(Other.m_HasError) {
107 if (m_HasError)
108 m_ErrState = Other.m_ErrState;
109 else
110 new (m_ValueBytes) T(*Other.valuePtr());
111#ifndef NDEBUG
112 m_Unchecked = Other.m_Unchecked;
113 Other.m_Unchecked = false;
114#endif
115 }
116
117 Result(Result&& Other) noexcept : m_HasError(Other.m_HasError) {
118 if (m_HasError)
119 m_ErrState = Other.m_ErrState;
120 else
121 new (m_ValueBytes) T(std::move(*Other.valuePtr()));
122#ifndef NDEBUG
123 m_Unchecked = Other.m_Unchecked;
124 Other.m_Unchecked = false;
125#endif
126 }
127
128 ~Result() {
129#ifndef NDEBUG
130 // Must-handle enforcement: an error-bearing Result that wasn't
131 // inspected aborts. Use .ignore() to drop on purpose.
132 if (m_Unchecked && m_HasError)
133 ResultAbort_UncheckedOnDtor(ErrorRef{m_ErrState});
134#endif
135 if (!m_HasError)
136 valuePtr()->~T();
137 }
138
139 Result& operator=(const Result&) = delete;
140 Result& operator=(Result&&) = delete;
141
142 /// Mark this Result checked without reading it -- for when you
143 /// genuinely want to drop the outcome.
144 void ignore() const { markChecked(); }
145
146 [[nodiscard]] bool ok() const {
147 markChecked();
148 return !m_HasError;
149 }
150 explicit operator bool() const { return ok(); }
151
152 [[nodiscard]] Status status() const {
153 markChecked();
154 return m_HasError ? Status::Failed : Status::Ok;
155 }
156
157 [[nodiscard]] ErrorRef error() const {
158 markChecked();
159 return m_HasError ? ErrorRef{m_ErrState} : ErrorRef{};
160 }
161
162 /// Value on Ok; aborts on Error. Use value_or for lenient semantics.
163 T value() const {
164 markChecked();
165 if (m_HasError)
167 return *valuePtr();
168 }
169
170 T value_or(T fallback) const {
171 markChecked();
172 return m_HasError ? std::move(fallback) : *valuePtr();
173 }
174};
175
176// Void specialization: no T storage, ErrorRef stored directly.
177
178template <> class [[nodiscard]] Result<void> {
179 const void* m_ErrState = nullptr;
180#ifndef NDEBUG
181 mutable bool m_Unchecked = true;
182 void markChecked() const { m_Unchecked = false; }
183#else
184 void markChecked() const {}
185#endif
186
187public:
188 Result() = default;
189 Result(ErrorRef E) : m_ErrState(E.state) {}
190
191 Result& operator=(const Result&) = delete;
192 Result& operator=(Result&&) = delete;
193
194 Result(const Result& O) : m_ErrState(O.m_ErrState) {
195#ifndef NDEBUG
196 m_Unchecked = O.m_Unchecked;
197 O.m_Unchecked = false;
198#endif
199 }
200
201 Result(Result&& O) noexcept : m_ErrState(O.m_ErrState) {
202#ifndef NDEBUG
203 // Transfer the must-handle obligation to the destination -- the
204 // default move leaves m_Unchecked=true on the source and the
205 // source's dtor would abort even though the value went to the
206 // caller (the bug seen when threading Results through tracing
207 // record() wrappers).
208 m_Unchecked = O.m_Unchecked;
209 O.m_Unchecked = false;
210#endif
211 O.m_ErrState = nullptr;
212 }
213
214 ~Result() {
215#ifndef NDEBUG
216 if (m_Unchecked && m_ErrState)
217 ResultAbort_UncheckedOnDtor(ErrorRef{m_ErrState});
218#endif
219 }
220
221 void ignore() const { markChecked(); }
222
223 [[nodiscard]] bool ok() const {
224 markChecked();
225 return m_ErrState == nullptr;
226 }
227 explicit operator bool() const { return ok(); }
228
229 [[nodiscard]] Status status() const {
230 markChecked();
231 return m_ErrState ? Status::Failed : Status::Ok;
232 }
233
234 [[nodiscard]] ErrorRef error() const {
235 markChecked();
236 return ErrorRef{m_ErrState};
237 }
238};
239
240} // namespace Cpp
241
242#endif // __cplusplus
243
244#endif // CPPINTEROP_ERROR_H
#define CPPINTEROP_API
Definition Box.h:70
void ResultAbort_UncheckedOnDtor(const ErrorRef &)
void ResultAbort_ValueOnError(const ErrorRef &)