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