10 changed files with 517 additions and 0 deletions
@ -0,0 +1,31 @@ |
|||||||
|
cmake_minimum_required(VERSION 3.21) |
||||||
|
project(leaf_examples LANGUAGES CXX) |
||||||
|
|
||||||
|
if(NOT CMAKE_CXX_STANDARD) |
||||||
|
set(CMAKE_CXX_STANDARD 17) |
||||||
|
endif() |
||||||
|
set(CMAKE_CXX_STANDARD_REQUIRED ON) |
||||||
|
|
||||||
|
find_package(Boost CONFIG COMPONENTS leaf) |
||||||
|
if(NOT TARGET Boost::leaf) |
||||||
|
find_package(Boost REQUIRED) |
||||||
|
set(LEAF_TARGET Boost::headers) |
||||||
|
else() |
||||||
|
set(LEAF_TARGET Boost::leaf) |
||||||
|
endif() |
||||||
|
|
||||||
|
set(EXAMPLES |
||||||
|
leaf_on_error_example |
||||||
|
leaf_mutable_block |
||||||
|
leaf_universal_handler |
||||||
|
) |
||||||
|
|
||||||
|
foreach(example IN LISTS EXAMPLES) |
||||||
|
add_executable(${example} src/${example}.cpp) |
||||||
|
target_link_libraries(${example} PRIVATE ${LEAF_TARGET}) |
||||||
|
endforeach() |
||||||
|
|
||||||
|
install( |
||||||
|
TARGETS ${EXAMPLES} |
||||||
|
RUNTIME DESTINATION bin |
||||||
|
) |
||||||
@ -0,0 +1,39 @@ |
|||||||
|
# syntax=docker/dockerfile:1 |
||||||
|
|
||||||
|
FROM debian:bookworm AS build |
||||||
|
|
||||||
|
RUN apt-get update && apt-get install -y --no-install-recommends \ |
||||||
|
build-essential \ |
||||||
|
cmake \ |
||||||
|
ninja-build \ |
||||||
|
git \ |
||||||
|
ca-certificates \ |
||||||
|
curl \ |
||||||
|
zip \ |
||||||
|
unzip \ |
||||||
|
tar \ |
||||||
|
pkg-config \ |
||||||
|
&& rm -rf /var/lib/apt/lists/* |
||||||
|
|
||||||
|
ENV VCPKG_ROOT=/opt/vcpkg |
||||||
|
RUN git clone --depth 1 https://github.com/microsoft/vcpkg.git ${VCPKG_ROOT} \ |
||||||
|
&& ${VCPKG_ROOT}/bootstrap-vcpkg.sh -disableMetrics |
||||||
|
|
||||||
|
WORKDIR /app |
||||||
|
COPY vcpkg.json CMakeLists.txt ./ |
||||||
|
COPY src ./src |
||||||
|
COPY scripts ./scripts |
||||||
|
|
||||||
|
RUN cmake -S . -B build -G Ninja \ |
||||||
|
-DCMAKE_TOOLCHAIN_FILE=${VCPKG_ROOT}/scripts/buildsystems/vcpkg.cmake \ |
||||||
|
-DCMAKE_BUILD_TYPE=Release \ |
||||||
|
&& cmake --build build \ |
||||||
|
&& cmake --install build --prefix /out |
||||||
|
|
||||||
|
FROM debian:bookworm-slim |
||||||
|
|
||||||
|
WORKDIR /app |
||||||
|
COPY --from=build /out/bin/ ./ |
||||||
|
COPY scripts/run_all.sh ./ |
||||||
|
|
||||||
|
ENTRYPOINT ["/app/run_all.sh"] |
||||||
@ -0,0 +1,78 @@ |
|||||||
|
# Boost.LEAF Diagnostic Propagation Examples |
||||||
|
|
||||||
|
Three small C++ programs demonstrating how [Boost.LEAF](https://boostorg.github.io/leaf/) |
||||||
|
propagates rich diagnostic information together with an error code, without throwing |
||||||
|
exceptions and without exceptions needing to be enabled in the codebase. |
||||||
|
|
||||||
|
- `src/leaf_on_error_example.cpp` — distinct context structs attached per scope with |
||||||
|
`leaf::on_error`; handlers matched by the exact set of available types. |
||||||
|
- `src/leaf_mutable_block.cpp` — a single mutable `diagnostic_context` struct, re-registered |
||||||
|
with `leaf::on_error` as the operation progresses, so the handler receives the latest |
||||||
|
snapshot of all fields. |
||||||
|
- `src/leaf_universal_handler.cpp` — handler selection: specific handlers first, generic |
||||||
|
"universal" handlers later, plus a final fallback. Shows how LEAF picks the best match. |
||||||
|
|
||||||
|
## Core idea |
||||||
|
|
||||||
|
LEAF returns errors as values of type `leaf::result<T>` instead of throwing exceptions. |
||||||
|
At each point where an error can occur, arbitrary diagnostic values (file names, user info, |
||||||
|
chunk numbers, timestamps, ...) are registered with `leaf::on_error(value)`. If |
||||||
|
`leaf::new_error(...)` is returned, all registered values are captured into the current |
||||||
|
error context, and the top level uses `leaf::try_handle_all(try_block, handlers...)` to let |
||||||
|
the handlers inspect whichever context types were active when the error was created. |
||||||
|
|
||||||
|
Handler matching is done purely on types: a handler requesting |
||||||
|
`(write_error const&, file_context const&, user_context const&)` will only run if the error |
||||||
|
carries exactly those registered values; a more general handler is used otherwise. This |
||||||
|
gives exception-like separation between "raising" a failure and "dealing with" it, but with |
||||||
|
explicit `result` values, no exceptions, and no RTTI dependency. |
||||||
|
|
||||||
|
The examples intentionally return distinct exit codes from the handlers so each one |
||||||
|
reports which handler actually matched. |
||||||
|
|
||||||
|
## Local build |
||||||
|
|
||||||
|
Requirements: CMake >= 3.21, a C++17 compiler, and [vcpkg](https://vcpkg.io) (git clone + |
||||||
|
bootstrap). The dependency is header-only `boost-leaf`. |
||||||
|
|
||||||
|
```bash |
||||||
|
git clone https://github.com/microsoft/vcpkg.git |
||||||
|
./vcpkg/bootstrap-vcpkg.sh |
||||||
|
|
||||||
|
cmake -S . -B build \ |
||||||
|
-DCMAKE_TOOLCHAIN_FILE=$PWD/vcpkg/scripts/buildsystems/vcpkg.cmake \ |
||||||
|
-DCMAKE_BUILD_TYPE=Release |
||||||
|
cmake --build build |
||||||
|
``` |
||||||
|
|
||||||
|
Run one of the examples: |
||||||
|
|
||||||
|
```bash |
||||||
|
./build/leaf_on_error_example |
||||||
|
``` |
||||||
|
|
||||||
|
If you already have Boost (>= 1.75) installed at the system level, plain |
||||||
|
`cmake -S . -B build` also works; the CMake project picks `Boost::leaf` from a vcpkg |
||||||
|
toolchain when available and falls back to the system header target otherwise. |
||||||
|
|
||||||
|
## Docker |
||||||
|
|
||||||
|
```bash |
||||||
|
docker build -t leaf-examples . |
||||||
|
docker run --rm leaf-examples |
||||||
|
``` |
||||||
|
|
||||||
|
The first (build) stage installs the toolchain, clones vcpkg, downloads `boost-leaf` |
||||||
|
through the `vcpkg.json` manifest and builds all three examples. The second (run) |
||||||
|
stage copies the binaries into a slim image and executes them one after another, |
||||||
|
printing the exit code of each. |
||||||
|
|
||||||
|
## Docker Compose (v2) |
||||||
|
|
||||||
|
```bash |
||||||
|
docker compose up --build |
||||||
|
``` |
||||||
|
|
||||||
|
runs the same build and then starts a container that executes all three examples. |
||||||
|
`docker compose build` builds without running; `docker compose run --rm leaf-examples` |
||||||
|
works too. |
||||||
@ -0,0 +1,8 @@ |
|||||||
|
services: |
||||||
|
leaf-examples: |
||||||
|
build: |
||||||
|
context: . |
||||||
|
dockerfile: Dockerfile |
||||||
|
network: host |
||||||
|
image: local/leaf-examples |
||||||
|
container_name: leaf-examples |
||||||
@ -0,0 +1,11 @@ |
|||||||
|
#!/bin/sh |
||||||
|
set -u |
||||||
|
|
||||||
|
apps="leaf_on_error_example leaf_mutable_block leaf_universal_handler" |
||||||
|
|
||||||
|
for app in ${apps}; do |
||||||
|
printf '\n=== %s ===\n' "${app}" |
||||||
|
"./${app}" |
||||||
|
printf '(exit code: %s)\n' "$?" |
||||||
|
done |
||||||
|
exit 0 |
||||||
@ -0,0 +1,102 @@ |
|||||||
|
// leaf_mutable_block.cpp
|
||||||
|
#include <boost/leaf.hpp> |
||||||
|
#include <ctime> |
||||||
|
#include <iostream> |
||||||
|
#include <optional> |
||||||
|
#include <string> |
||||||
|
#include <utility> |
||||||
|
|
||||||
|
namespace leaf = boost::leaf; |
||||||
|
|
||||||
|
struct error_tag {}; |
||||||
|
struct open_error {}; |
||||||
|
struct write_error {}; |
||||||
|
|
||||||
|
struct diagnostic_context { |
||||||
|
std::string file; |
||||||
|
std::optional<std::string> user; |
||||||
|
std::optional<int> privileges; |
||||||
|
std::optional<int> chunk; |
||||||
|
std::optional<std::size_t> chunk_size; |
||||||
|
std::optional<std::time_t> timestamp; |
||||||
|
}; |
||||||
|
|
||||||
|
leaf::result<void> write_chunk(int chunk, std::size_t size) |
||||||
|
{ |
||||||
|
if (chunk == 3) |
||||||
|
return leaf::new_error(write_error{}); |
||||||
|
|
||||||
|
return {}; |
||||||
|
} |
||||||
|
|
||||||
|
leaf::result<void> process_file(std::string path, std::string user) |
||||||
|
{ |
||||||
|
error_tag tag; |
||||||
|
diagnostic_context diag; |
||||||
|
|
||||||
|
diag.file = path; |
||||||
|
diag.user = std::move(user); |
||||||
|
diag.privileges = 0600; |
||||||
|
|
||||||
|
[[maybe_unused]] auto tag_ctx = leaf::on_error(tag); |
||||||
|
// on_error captures a snapshot of diag, not a reference, so any
|
||||||
|
// fields mutated later would be invisible unless re-registered.
|
||||||
|
[[maybe_unused]] auto diag_ctx = leaf::on_error(diag); |
||||||
|
|
||||||
|
if (diag.file.empty()) |
||||||
|
return leaf::new_error(open_error{}); |
||||||
|
|
||||||
|
for (int i = 0; i != 5; ++i) { |
||||||
|
diag.chunk = i; |
||||||
|
diag.chunk_size = 4096; |
||||||
|
diag.timestamp = std::time(nullptr); |
||||||
|
// Re-registering publishes the updated snapshot; the newest
|
||||||
|
// registration wins for the handler.
|
||||||
|
[[maybe_unused]] auto refresh = leaf::on_error(diag); |
||||||
|
|
||||||
|
auto r = write_chunk(i, 4096); |
||||||
|
if (!r) |
||||||
|
return r; |
||||||
|
} |
||||||
|
|
||||||
|
return {}; |
||||||
|
} |
||||||
|
|
||||||
|
int main() |
||||||
|
{ |
||||||
|
return leaf::try_handle_all( |
||||||
|
[]() -> leaf::result<int> { |
||||||
|
BOOST_LEAF_CHECK(process_file("data.bin", "alice")); |
||||||
|
return 0; |
||||||
|
}, |
||||||
|
|
||||||
|
[](error_tag const&, diagnostic_context const& d) { |
||||||
|
// d is the newest snapshot taken before the error was raised,
|
||||||
|
// which is why chunk/chunk_size/timestamp are filled in here.
|
||||||
|
std::cerr << "operation failed: file=" << d.file; |
||||||
|
|
||||||
|
if (d.user) |
||||||
|
std::cerr << ", user=" << *d.user; |
||||||
|
|
||||||
|
if (d.privileges) |
||||||
|
std::cerr << ", privileges=" << *d.privileges; |
||||||
|
|
||||||
|
if (d.chunk) |
||||||
|
std::cerr << ", chunk=" << *d.chunk; |
||||||
|
|
||||||
|
if (d.chunk_size) |
||||||
|
std::cerr << ", chunk_size=" << *d.chunk_size; |
||||||
|
|
||||||
|
if (d.timestamp) |
||||||
|
std::cerr << ", time=" << *d.timestamp; |
||||||
|
|
||||||
|
std::cerr << '\n'; |
||||||
|
return 1; |
||||||
|
}, |
||||||
|
|
||||||
|
[] { |
||||||
|
std::cerr << "operation failed before diagnostic context existed\n"; |
||||||
|
return 2; |
||||||
|
} |
||||||
|
); |
||||||
|
} |
||||||
@ -0,0 +1,130 @@ |
|||||||
|
// leaf_on_error_example.cpp
|
||||||
|
#include <boost/leaf.hpp> |
||||||
|
#include <ctime> |
||||||
|
#include <iostream> |
||||||
|
#include <string> |
||||||
|
#include <utility> |
||||||
|
|
||||||
|
namespace leaf = boost::leaf; |
||||||
|
|
||||||
|
struct error_tag {}; |
||||||
|
struct open_error {}; |
||||||
|
struct write_error {}; |
||||||
|
|
||||||
|
struct file_context { |
||||||
|
std::string path; |
||||||
|
}; |
||||||
|
|
||||||
|
struct user_context { |
||||||
|
std::string name; |
||||||
|
int privileges; |
||||||
|
}; |
||||||
|
|
||||||
|
struct chunk_context { |
||||||
|
int chunk; |
||||||
|
std::size_t size; |
||||||
|
std::time_t timestamp; |
||||||
|
}; |
||||||
|
|
||||||
|
leaf::result<void> write_chunk(int chunk, std::size_t size) |
||||||
|
{ |
||||||
|
chunk_context cc{chunk, size, std::time(nullptr)}; |
||||||
|
// Registers cc to be captured only if an error unwinds this scope.
|
||||||
|
// On success the registration is discarded.
|
||||||
|
[[maybe_unused]] auto ctx = leaf::on_error(cc); |
||||||
|
|
||||||
|
if (chunk == 3) |
||||||
|
// The context registered above rides along with this error
|
||||||
|
// even though handlers live several frames up.
|
||||||
|
return leaf::new_error(write_error{}); |
||||||
|
|
||||||
|
return {}; |
||||||
|
} |
||||||
|
|
||||||
|
leaf::result<void> process_file(std::string path, std::string user) |
||||||
|
{ |
||||||
|
error_tag tag; |
||||||
|
file_context fc{std::move(path)}; |
||||||
|
user_context uc{std::move(user), 0600}; |
||||||
|
|
||||||
|
[[maybe_unused]] auto tag_ctx = leaf::on_error(tag); |
||||||
|
[[maybe_unused]] auto file_ctx = leaf::on_error(fc); |
||||||
|
[[maybe_unused]] auto user_ctx = leaf::on_error(uc); |
||||||
|
|
||||||
|
// write_chunk knows nothing about the contexts registered here;
|
||||||
|
// LEAF attaches them automatically to any error it propagates.
|
||||||
|
for (int i = 0; i != 5; ++i) { |
||||||
|
auto r = write_chunk(i, 4096); |
||||||
|
if (!r) |
||||||
|
return r; |
||||||
|
} |
||||||
|
|
||||||
|
return {}; |
||||||
|
} |
||||||
|
|
||||||
|
int main() |
||||||
|
{ |
||||||
|
return leaf::try_handle_all( |
||||||
|
[]() -> leaf::result<int> { |
||||||
|
// Try block and handlers must return the same type.
|
||||||
|
BOOST_LEAF_CHECK(process_file("data.bin", "alice")); |
||||||
|
return 0; |
||||||
|
}, |
||||||
|
|
||||||
|
// A handler runs only if the error carries every type it
|
||||||
|
// requests; handlers below with fewer parameters match the
|
||||||
|
// same errors when richer contexts were never registered.
|
||||||
|
[](write_error const&, |
||||||
|
error_tag const&, |
||||||
|
file_context const& f, |
||||||
|
user_context const& u, |
||||||
|
chunk_context const& c) { |
||||||
|
std::cerr << "write failed: " |
||||||
|
<< "file=" << f.path |
||||||
|
<< ", user=" << u.name |
||||||
|
<< ", chunk=" << c.chunk |
||||||
|
<< ", size=" << c.size |
||||||
|
<< ", time=" << c.timestamp |
||||||
|
<< '\n'; |
||||||
|
return 1; |
||||||
|
}, |
||||||
|
|
||||||
|
[](open_error const&, |
||||||
|
error_tag const&, |
||||||
|
file_context const& f, |
||||||
|
user_context const& u) { |
||||||
|
std::cerr << "open failed: " |
||||||
|
<< "file=" << f.path |
||||||
|
<< ", user=" << u.name |
||||||
|
<< '\n'; |
||||||
|
return 2; |
||||||
|
}, |
||||||
|
|
||||||
|
[](error_tag const&, |
||||||
|
file_context const& f, |
||||||
|
user_context const& u, |
||||||
|
chunk_context const& c) { |
||||||
|
std::cerr << "unknown error: " |
||||||
|
<< "file=" << f.path |
||||||
|
<< ", user=" << u.name |
||||||
|
<< ", chunk=" << c.chunk |
||||||
|
<< '\n'; |
||||||
|
return 3; |
||||||
|
}, |
||||||
|
|
||||||
|
[](error_tag const&, |
||||||
|
file_context const& f, |
||||||
|
user_context const& u) { |
||||||
|
std::cerr << "unknown error: " |
||||||
|
<< "file=" << f.path |
||||||
|
<< ", user=" << u.name |
||||||
|
<< '\n'; |
||||||
|
return 4; |
||||||
|
}, |
||||||
|
|
||||||
|
[] { |
||||||
|
std::cerr << "unknown error before file context\n"; |
||||||
|
return 5; |
||||||
|
} |
||||||
|
); |
||||||
|
} |
||||||
@ -0,0 +1,106 @@ |
|||||||
|
// leaf_universal_handler.cpp
|
||||||
|
#include <boost/leaf.hpp> |
||||||
|
#include <iostream> |
||||||
|
#include <string> |
||||||
|
#include <utility> |
||||||
|
|
||||||
|
namespace leaf = boost::leaf; |
||||||
|
|
||||||
|
struct error_tag {}; |
||||||
|
struct early_error {}; |
||||||
|
struct unknown_stage_error {}; |
||||||
|
|
||||||
|
struct operation_context { |
||||||
|
std::string operation; |
||||||
|
std::string user; |
||||||
|
}; |
||||||
|
|
||||||
|
struct stage_context { |
||||||
|
std::string stage; |
||||||
|
}; |
||||||
|
|
||||||
|
leaf::result<void> stage_two() |
||||||
|
{ |
||||||
|
// stage_context exists only in this scope; handlers above cannot
|
||||||
|
// see it unless an error escapes while this registration is active.
|
||||||
|
stage_context sc{"stage_two"}; |
||||||
|
[[maybe_unused]] auto ctx = leaf::on_error(sc); |
||||||
|
|
||||||
|
return leaf::new_error(unknown_stage_error{}); |
||||||
|
} |
||||||
|
|
||||||
|
leaf::result<void> run(bool fail_early) |
||||||
|
{ |
||||||
|
error_tag tag; |
||||||
|
operation_context oc{"universal_handler", "alice"}; |
||||||
|
|
||||||
|
[[maybe_unused]] auto tag_ctx = leaf::on_error(tag); |
||||||
|
[[maybe_unused]] auto op_ctx = leaf::on_error(oc); |
||||||
|
|
||||||
|
if (fail_early) |
||||||
|
return leaf::new_error(early_error{}); |
||||||
|
|
||||||
|
return stage_two(); |
||||||
|
} |
||||||
|
|
||||||
|
int execute(bool fail_early) |
||||||
|
{ |
||||||
|
return leaf::try_handle_all( |
||||||
|
[fail_early]() -> leaf::result<int> { |
||||||
|
BOOST_LEAF_CHECK(run(fail_early)); |
||||||
|
return 0; |
||||||
|
}, |
||||||
|
|
||||||
|
// Specific handler: runs only for early_error.
|
||||||
|
[](early_error const&, |
||||||
|
error_tag const&, |
||||||
|
operation_context const& op) { |
||||||
|
std::cerr << "early error: " |
||||||
|
<< "operation=" << op.operation |
||||||
|
<< ", user=" << op.user |
||||||
|
<< '\n'; |
||||||
|
return 1; |
||||||
|
}, |
||||||
|
|
||||||
|
// Universal handler: no specific error type, so it matches any
|
||||||
|
// error that carries error_tag + operation_context. This overload
|
||||||
|
// is selected when the error also carries stage_context (i.e. it
|
||||||
|
// came from stage_two). LEAF picks the handler whose parameter
|
||||||
|
// list matches the most context available.
|
||||||
|
[](error_tag const&, |
||||||
|
operation_context const& op, |
||||||
|
stage_context const& st) { |
||||||
|
std::cerr << "universal handler with stage: " |
||||||
|
<< "operation=" << op.operation |
||||||
|
<< ", user=" << op.user |
||||||
|
<< ", stage=" << st.stage |
||||||
|
<< '\n'; |
||||||
|
return 2; |
||||||
|
}, |
||||||
|
|
||||||
|
// Same universal shape minus stage_context: matches errors that
|
||||||
|
// never passed through stage_two.
|
||||||
|
[](error_tag const&, |
||||||
|
operation_context const& op) { |
||||||
|
std::cerr << "universal handler without stage: " |
||||||
|
<< "operation=" << op.operation |
||||||
|
<< ", user=" << op.user |
||||||
|
<< '\n'; |
||||||
|
return 3; |
||||||
|
}, |
||||||
|
|
||||||
|
// Fallback for errors carrying nothing the other handlers accept
|
||||||
|
// (e.g. thrown exceptions not produced by new_error).
|
||||||
|
[] { |
||||||
|
std::cerr << "fallback handler\n"; |
||||||
|
return 4; |
||||||
|
} |
||||||
|
); |
||||||
|
} |
||||||
|
|
||||||
|
int main() |
||||||
|
{ |
||||||
|
execute(false); |
||||||
|
execute(true); |
||||||
|
return 0; |
||||||
|
} |
||||||
Loading…
Reference in new issue