diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml
index 3e8e97d15..5dd2bcbf6 100644
--- a/.github/workflows/coverage.yml
+++ b/.github/workflows/coverage.yml
@@ -3,10 +3,25 @@ name: coverage
env:
GXX_VERSION: 12
TESTS_DIR: out/tests
- COVERAGE_FILE: coverage.info
+ COBERTURA_REPORT: cobertura.xml
+ COVERALLS_REPORT: coveralls.json
+ HTML_REPORT_DIR: html/
COVERAGE_ARTIFACT_NAME: coverage-report
-on: [push, pull_request]
+on:
+ push:
+ branches:
+ - master
+ - development
+ paths-ignore:
+ - 'README.md'
+ - 'docs/'
+ pull_request:
+ branches:
+ - '**'
+ paths-ignore:
+ - 'README.md'
+ - 'docs/'
jobs:
create-coverage-report:
@@ -14,16 +29,19 @@ jobs:
steps:
- uses: actions/checkout@v4
- - name: Install compiler
+ - name: Install prerequisites
run: |
sudo apt-get update
+ sudo apt-get install pip -y
+ sudo pip install gcovr
sudo apt-get install g++-${{ env.GXX_VERSION }} -y
sudo update-alternatives --install /usr/bin/g++ g++ /usr/bin/g++-${{ env.GXX_VERSION }} ${{ env.GXX_VERSION }}00 --slave /usr/bin/gcc gcc /usr/bin/gcc-${{ env.GXX_VERSION }}
sudo update-alternatives --install /usr/bin/gcov gcov /usr/bin/gcov-${{ env.GXX_VERSION }} ${{ env.GXX_VERSION }}00
- name: Compile tests
env:
- CXXFLAGS: "--coverage -fno-inline -fprofile-abs-path"
+ LDFLAGS: "-fprofile-arcs"
+ CXXFLAGS: "-g -O0 --coverage -fno-inline -fprofile-abs-path -fkeep-inline-functions -fkeep-static-functions"
CC: gcc
CXX: g++
run: |
@@ -33,19 +51,34 @@ jobs:
- name: Run tests
env:
CTEST_OUTPUT_ON_FAILURE: 1
- run: ctest --test-dir ${{ env.TESTS_DIR }} --timeout 30 -C Debug -j4
+ run: ctest --test-dir ${{env.TESTS_DIR}} -C Debug -j4
- - name: Collect data
+ - name: Run gcovr
run: |
- sudo apt-get install lcov -y
- lcov --capture -d ${{env.TESTS_DIR}} -o ${{env.COVERAGE_FILE}} --include "$PWD/include/*"
- lcov -l ${{env.COVERAGE_FILE}}
+ gcovr --root ${{env.TESTS_DIR}} --filter include/Simple-Utility --keep -j 4 \
+ --exclude-lines-by-pattern "\s*assert\(" \
+ --exclude-unreachable-branches \
+ --exclude-noncode-lines \
+ --exclude-throw-branches \
+ --decisions \
+ --cobertura ${{env.COBERTURA_REPORT}} --cobertura-pretty \
+ --html-nested ${{env.HTML_REPORT_DIR}} --html-title "Simple-Utility Coverage Report" \
+ --coveralls ${{env.COVERALLS_REPORT}} --coveralls-pretty
- - name: Upload artifacts
+ - name: Upload gcov coverage report artifacts
+ uses: actions/upload-artifact@v3
+ with:
+ name: gcov-files
+ path: "${{env.TESTS_DIR}}/*.gcov"
+
+ - name: Upload generated report artifacts
uses: actions/upload-artifact@v3
with:
name: ${{env.COVERAGE_ARTIFACT_NAME}}
- path: ${{env.COVERAGE_FILE}}
+ path: |
+ ${{env.COBERTURA_REPORT}}
+ ${{env.COVERALLS_REPORT}}
+ ${{env.HTML_REPORT_DIR}}
codacy-report:
needs: create-coverage-report
@@ -60,8 +93,8 @@ jobs:
- name: Upload coverage to Codacy
uses: codacy/codacy-coverage-reporter-action@v1
with:
- project-token: ${{ secrets.CODACY_PROJECT_TOKEN }}
- coverage-reports: ${{ env.COVERAGE_FILE }}
+ project-token: ${{secrets.CODACY_PROJECT_TOKEN}}
+ coverage-reports: ${{env.COBERTURA_REPORT}}
codecov-report:
needs: create-coverage-report
@@ -77,8 +110,8 @@ jobs:
uses: codecov/codecov-action@v3
with:
name: $GITHUB_REPOSITORY
- token: ${{ secrets.CODECOV_TOKEN }}
- files: ${{ env.COVERAGE_FILE }}
+ token: ${{secrets.CODECOV_TOKEN}}
+ files: ${{env.COBERTURA_REPORT}}
fail_ci_if_error: true
verbose: true
@@ -96,6 +129,6 @@ jobs:
uses: coverallsapp/github-action@v2
with:
github-token: ${{secrets.GITHUB_TOKEN}}
- file: ${{env.COVERAGE_FILE}}
- format: lcov
+ file: ${{env.COVERALLS_REPORT}}
+ format: coveralls
fail-on-error: true
diff --git a/.github/workflows/run_benchmarks.yml b/.github/workflows/run_benchmarks.yml
new file mode 100644
index 000000000..ce4896b66
--- /dev/null
+++ b/.github/workflows/run_benchmarks.yml
@@ -0,0 +1,161 @@
+name: run benchmarks
+
+env:
+ BUILD_DIR: out # root build directory
+ BENCHMARK_DIR: benchmarks # relative directory below BUILD_DIR
+ ARTIFACTS_DIR: artifacts # relative directory below BENCHMARK_DIR
+
+on:
+ push:
+ branches:
+ - master
+ - development
+ paths-ignore:
+ - 'README.md'
+ - 'docs/'
+ pull_request:
+ branches:
+ - '**'
+ paths-ignore:
+ - 'README.md'
+ - 'docs/'
+
+jobs:
+ ubuntu-22_04:
+ runs-on: ubuntu-22.04
+
+ strategy:
+ fail-fast: false
+ matrix:
+ build_mode: [Release]
+ compiler:
+ - pkg: g++-12
+ exe: g++-12
+ - pkg: g++-11
+ exe: g++-11
+ - pkg: g++-10
+ exe: g++-10
+ - pkg: clang-14
+ exe: clang++-14
+ - pkg: clang-13
+ exe: clang++-13
+ - pkg: clang-12
+ exe: clang++-12
+ - pkg: clang-11
+ exe: clang++-11
+
+ steps:
+ - uses: actions/checkout@v3
+ - name: Install compiler
+ run: |
+ sudo apt-get update
+ sudo apt-get install ${{ matrix.compiler.pkg }} -y
+ - name: Compile
+ env:
+ CXX: ${{ matrix.compiler.exe }}
+ run: |
+ cmake -DCMAKE_BUILD_TYPE=${{ matrix.build_mode }} -DSIMPLE_UTILITY_BUILD_BENCHMARKS=Yes -B ${{ env.BUILD_DIR }} -S .
+ cmake --build ${{ env.BUILD_DIR }} -j4
+ - name: Run benchmarks
+ run: "${{env.BUILD_DIR}}/${{env.BENCHMARK_DIR}}/Simple-Utility-Benchmarks"
+ - name: Upload generated report artifacts
+ uses: actions/upload-artifact@v3
+ with:
+ path: "${{env.ARTIFACTS_DIR}}"
+
+
+ ubuntu-20_04:
+ runs-on: ubuntu-20.04
+
+ strategy:
+ fail-fast: false
+ matrix:
+ build_mode: [Release]
+ compiler:
+ - pkg: g++-11
+ exe: g++-11
+ - pkg: g++-10
+ exe: g++-10
+ - pkg: clang-12
+ exe: clang++-12
+ - pkg: clang-11
+ exe: clang++-11
+ - pkg: clang-10
+ exe: clang++-10
+
+ steps:
+ - uses: actions/checkout@v3
+ - name: Install compiler
+ run: |
+ sudo apt-get update
+ sudo apt-get install ${{ matrix.compiler.pkg }} -y
+ - name: Compile
+ env:
+ CXX: ${{ matrix.compiler.exe }}
+ run: |
+ cmake -DCMAKE_BUILD_TYPE=${{ matrix.build_mode }} -DSIMPLE_UTILITY_BUILD_BENCHMARKS=Yes -B ${{ env.BUILD_DIR }} -S .
+ cmake --build ${{ env.BUILD_DIR }} -j4
+ - name: Run benchmarks
+ run: "${{env.BUILD_DIR}}/${{env.BENCHMARK_DIR}}/Simple-Utility-Benchmarks"
+ - name: Upload generated report artifacts
+ uses: actions/upload-artifact@v3
+ with:
+ path: "${{env.ARTIFACTS_DIR}}"
+
+
+ windows_2022:
+ runs-on: windows-2022
+
+ strategy:
+ fail-fast: false
+ matrix:
+ build_mode: [Release]
+ toolset: [v142, v143, ClangCl]
+
+ steps:
+ - uses: actions/checkout@v3
+ - name: Compile
+ run: |
+ cmake -G"Visual Studio 17 2022" `
+ -T${{matrix.toolset}} `
+ -DCMAKE_BUILD_TYPE=${{matrix.build_mode}} `
+ -DSIMPLE_UTILITY_BUILD_BENCHMARKS=Yes `
+ -B${{env.BUILD_DIR}} `
+ -Ax64 `
+ -S .
+ cmake --build ${{ env.BUILD_DIR }} --config ${{ matrix.build_mode }} -j4
+ - name: Run benchmarks
+ run: ".\\${{env.BUILD_DIR}}\\${{env.BENCHMARK_DIR}}\\${{matrix.build_mode}}\\Simple-Utility-Benchmarks.exe"
+ - name: Upload generated report artifacts
+ uses: actions/upload-artifact@v3
+ with:
+ path: "${{env.ARTIFACTS_DIR}}"
+
+
+ windows_2019:
+ runs-on: windows-2019
+
+ strategy:
+ fail-fast: false
+ matrix:
+ build_mode: [Release]
+ toolset: [v142, ClangCl]
+
+ steps:
+ - uses: actions/checkout@v3
+ - name: Compile
+ run: |
+ cmake -G"Visual Studio 16 2019" `
+ -T${{matrix.toolset}} `
+ -DCMAKE_BUILD_TYPE=${{matrix.build_mode}} `
+ -DSIMPLE_UTILITY_BUILD_BENCHMARKS=Yes `
+ -B${{env.BUILD_DIR}} `
+ -Ax64 `
+ -S .
+ cmake --build ${{env.BUILD_DIR}} --config ${{matrix.build_mode}} -j4
+ - name: Run benchmarks
+ run: ".\\${{env.BUILD_DIR}}\\${{env.BENCHMARK_DIR}}\\${{matrix.build_mode}}\\Simple-Utility-Benchmarks.exe"
+ - name: Upload generated report artifacts
+ uses: actions/upload-artifact@v3
+ with:
+ path: "${{env.ARTIFACTS_DIR}}"
diff --git a/.github/workflows/run_tests.yml b/.github/workflows/run_tests.yml
index 88ecae715..454e4b6fb 100644
--- a/.github/workflows/run_tests.yml
+++ b/.github/workflows/run_tests.yml
@@ -6,9 +6,18 @@ env:
on:
push:
- branches: [master, development]
+ branches:
+ - master
+ - development
+ paths-ignore:
+ - 'README.md'
+ - 'docs/'
pull_request:
- branches: [master, development]
+ branches:
+ - '**'
+ paths-ignore:
+ - 'README.md'
+ - 'docs/'
jobs:
ubuntu-22_04:
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 1c481d3f8..493c66adb 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -32,6 +32,12 @@ if (SIMPLE_UTILITY_BUILD_TESTS OR CMAKE_SOURCE_DIR STREQUAL Simple-Utility_SOURC
add_subdirectory("tests")
endif()
+OPTION(SIMPLE_UTILITY_BUILD_BENCHMARKS "Determines whether benchmarks will be built." OFF)
+if (SIMPLE_UTILITY_BUILD_BENCHMARKS)
+ include(CTest)
+ add_subdirectory("benchmarks")
+endif()
+
option(SIMPLE_UTILITY_GEN_DOCS_ENABLED "Enables the GenerateDocs target." OFF)
if (SIMPLE_UTILITY_GEN_DOCS_ENABLED)
add_subdirectory("docs")
diff --git a/CMakeSettings.json b/CMakeSettings.json
index bc058be69..bc43b563e 100644
--- a/CMakeSettings.json
+++ b/CMakeSettings.json
@@ -1,139 +1,162 @@
{
- "configurations": [
- {
- "name": "x64-Debug",
- "generator": "Ninja",
- "configurationType": "Debug",
- "inheritEnvironments": [ "msvc_x64_x64" ],
- "buildRoot": "${projectDir}\\out\\build\\${name}",
- "installRoot": "${projectDir}\\out\\install\\${name}",
- "cmakeCommandArgs": "",
- "buildCommandArgs": "",
- "ctestCommandArgs": "",
- "variables": [
- {
- "name": "CMAKE_EXPORT_COMPILE_COMMANDS",
- "value": "True",
- "type": "BOOL"
- }
- ]
- },
- {
- "name": "clang-x64-Debug",
- "generator": "Ninja",
- "configurationType": "Debug",
- "buildRoot": "${projectDir}\\out\\build\\${name}",
- "installRoot": "${projectDir}\\out\\install\\${name}",
- "cmakeCommandArgs": "",
- "buildCommandArgs": "",
- "ctestCommandArgs": "",
- "inheritEnvironments": [ "clang_cl_x64_x64" ],
- "variables": [
- {
- "name": "Simple-Utility_BUILD_TESTS",
- "value": "True",
- "type": "BOOL"
- }
- ]
- },
- {
- "name": "Linux-GCC-Debug",
- "generator": "Ninja",
- "configurationType": "Debug",
- "cmakeExecutable": "cmake",
- "remoteCopySourcesExclusionList": [ ".vs", ".git", "out" ],
- "cmakeCommandArgs": "",
- "buildCommandArgs": "",
- "ctestCommandArgs": "",
- "inheritEnvironments": [ "linux_x64" ],
- "remoteMachineName": "${defaultRemoteMachineName}",
- "remoteCMakeListsRoot": "$HOME/.vs/${projectDirName}/${workspaceHash}/src",
- "remoteBuildRoot": "$HOME/.vs/${projectDirName}/${workspaceHash}/out/build/${name}",
- "remoteInstallRoot": "$HOME/.vs/${projectDirName}/${workspaceHash}/out/install/${name}",
- "remoteCopySources": true,
- "rsyncCommandArgs": "-t --delete --delete-excluded",
- "remoteCopyBuildOutput": false,
- "remoteCopySourcesMethod": "rsync",
- "variables": [
- {
- "name": "CATCH_BUILD_TESTING",
- "value": "False",
- "type": "BOOL"
- }
- ]
- },
- {
- "name": "Linux-Clang-Debug",
- "generator": "Ninja",
- "configurationType": "Debug",
- "cmakeExecutable": "cmake",
- "remoteCopySourcesExclusionList": [ ".vs", ".git", "out" ],
- "cmakeCommandArgs": "",
- "buildCommandArgs": "",
- "ctestCommandArgs": "",
- "inheritEnvironments": [ "linux_clang_x64" ],
- "variables": [
- {
- "name": "CATCH_BUILD_TESTING",
- "value": "False",
- "type": "BOOL"
- }
- ],
- "remoteMachineName": "${defaultRemoteMachineName}",
- "remoteCMakeListsRoot": "$HOME/.vs/${projectDirName}/${workspaceHash}/src",
- "remoteBuildRoot": "$HOME/.vs/${projectDirName}/${workspaceHash}/out/build/${name}",
- "remoteInstallRoot": "$HOME/.vs/${projectDirName}/${workspaceHash}/out/install/${name}",
- "remoteCopySources": true,
- "rsyncCommandArgs": "-t --delete --delete-excluded",
- "remoteCopyBuildOutput": false,
- "remoteCopySourcesMethod": "rsync"
- },
- {
- "name": "GenerateDocs",
- "generator": "Ninja",
- "buildRoot": "${projectDir}\\out\\build\\${name}",
- "installRoot": "${projectDir}\\out\\install\\${name}",
- "buildCommandArgs": "GenerateDocs",
- "ctestCommandArgs": "",
- "inheritEnvironments": [ "msvc_x64_x64" ],
- "variables": [
- {
- "name": "SIMPLE_UTILITY_GEN_DOCS_ENABLED",
- "value": "True",
- "type": "BOOL"
- }
- ]
- },
- {
- "name": "Linux-GCC-12-Debug",
- "generator": "Ninja",
- "configurationType": "Debug",
- "cmakeExecutable": "cmake",
- "remoteCopySourcesExclusionList": [ ".vs", ".git", "out" ],
- "cmakeCommandArgs": "",
- "buildCommandArgs": "",
- "ctestCommandArgs": "",
- "inheritEnvironments": [ "linux_x64" ],
- "variables": [
- {
- "name": "CATCH_BUILD_TESTING",
- "value": "False",
- "type": "BOOL"
- },
- {
- "name": "CMAKE_CXX_COMPILER",
- "value": "/usr/bin/g++-12",
- "type": "FILEPATH"
- }
- ],
- "remoteMachineName": "${defaultRemoteMachineName}",
- "remoteCMakeListsRoot": "$HOME/.vs/${projectDirName}/${workspaceHash}/src",
- "remoteBuildRoot": "$HOME/.vs/${projectDirName}/${workspaceHash}/out/build/${name}",
- "remoteInstallRoot": "$HOME/.vs/${projectDirName}/${workspaceHash}/out/install/${name}",
- "remoteCopySources": true,
- "rsyncCommandArgs": "-t --delete --delete-excluded",
- "remoteCopyBuildOutput": false,
- "remoteCopySourcesMethod": "rsync"
- }
- ]
+ "configurations": [
+ {
+ "name": "x64-Debug",
+ "generator": "Ninja",
+ "configurationType": "Debug",
+ "inheritEnvironments": [ "msvc_x64_x64" ],
+ "buildRoot": "${projectDir}\\out\\build\\${name}",
+ "installRoot": "${projectDir}\\out\\install\\${name}",
+ "cmakeCommandArgs": "",
+ "buildCommandArgs": "",
+ "ctestCommandArgs": "",
+ "variables": [
+ {
+ "name": "CMAKE_EXPORT_COMPILE_COMMANDS",
+ "value": "True",
+ "type": "BOOL"
+ }
+ ]
+ },
+ {
+ "name": "clang-x64-Debug",
+ "generator": "Ninja",
+ "configurationType": "Debug",
+ "buildRoot": "${projectDir}\\out\\build\\${name}",
+ "installRoot": "${projectDir}\\out\\install\\${name}",
+ "cmakeCommandArgs": "",
+ "buildCommandArgs": "",
+ "ctestCommandArgs": "",
+ "inheritEnvironments": [ "clang_cl_x64_x64" ],
+ "variables": [
+ {
+ "name": "CMAKE_C_COMPILER_WORKS",
+ "value": "True",
+ "type": "BOOL"
+ }
+ ]
+ },
+ {
+ "name": "Linux-GCC-Debug",
+ "generator": "Ninja",
+ "configurationType": "Debug",
+ "cmakeExecutable": "cmake",
+ "remoteCopySourcesExclusionList": [ ".vs", ".git", "out" ],
+ "cmakeCommandArgs": "",
+ "buildCommandArgs": "",
+ "ctestCommandArgs": "",
+ "inheritEnvironments": [ "linux_x64" ],
+ "remoteMachineName": "${defaultRemoteMachineName}",
+ "remoteCMakeListsRoot": "$HOME/.vs/${projectDirName}/${workspaceHash}/src",
+ "remoteBuildRoot": "$HOME/.vs/${projectDirName}/${workspaceHash}/out/build/${name}",
+ "remoteInstallRoot": "$HOME/.vs/${projectDirName}/${workspaceHash}/out/install/${name}",
+ "remoteCopySources": true,
+ "rsyncCommandArgs": "-t --delete --delete-excluded",
+ "remoteCopyBuildOutput": false,
+ "remoteCopySourcesMethod": "rsync",
+ "variables": [
+ {
+ "name": "CATCH_BUILD_TESTING",
+ "value": "False",
+ "type": "BOOL"
+ }
+ ]
+ },
+ {
+ "name": "Linux-Clang-Debug",
+ "generator": "Ninja",
+ "configurationType": "Debug",
+ "cmakeExecutable": "cmake",
+ "remoteCopySourcesExclusionList": [ ".vs", ".git", "out" ],
+ "cmakeCommandArgs": "",
+ "buildCommandArgs": "",
+ "ctestCommandArgs": "",
+ "inheritEnvironments": [ "linux_clang_x64" ],
+ "variables": [
+ {
+ "name": "CATCH_BUILD_TESTING",
+ "value": "False",
+ "type": "BOOL"
+ },
+ {
+ "name": "CMAKE_C_COMPILER_WORKS",
+ "value": "True",
+ "type": "BOOL"
+ }
+ ],
+ "remoteMachineName": "${defaultRemoteMachineName}",
+ "remoteCMakeListsRoot": "$HOME/.vs/${projectDirName}/${workspaceHash}/src",
+ "remoteBuildRoot": "$HOME/.vs/${projectDirName}/${workspaceHash}/out/build/${name}",
+ "remoteInstallRoot": "$HOME/.vs/${projectDirName}/${workspaceHash}/out/install/${name}",
+ "remoteCopySources": true,
+ "rsyncCommandArgs": "-t --delete --delete-excluded",
+ "remoteCopyBuildOutput": false,
+ "remoteCopySourcesMethod": "rsync"
+ },
+ {
+ "name": "GenerateDocs",
+ "generator": "Ninja",
+ "buildRoot": "${projectDir}\\out\\build\\${name}",
+ "installRoot": "${projectDir}\\out\\install\\${name}",
+ "buildCommandArgs": "GenerateDocs",
+ "ctestCommandArgs": "",
+ "inheritEnvironments": [ "msvc_x64_x64" ],
+ "variables": [
+ {
+ "name": "SIMPLE_UTILITY_GEN_DOCS_ENABLED",
+ "value": "True",
+ "type": "BOOL"
+ }
+ ]
+ },
+ {
+ "name": "Linux-GCC-12-Debug",
+ "generator": "Ninja",
+ "configurationType": "Debug",
+ "cmakeExecutable": "cmake",
+ "remoteCopySourcesExclusionList": [ ".vs", ".git", "out" ],
+ "cmakeCommandArgs": "",
+ "buildCommandArgs": "",
+ "ctestCommandArgs": "",
+ "inheritEnvironments": [ "linux_x64" ],
+ "variables": [
+ {
+ "name": "CATCH_BUILD_TESTING",
+ "value": "False",
+ "type": "BOOL"
+ },
+ {
+ "name": "CMAKE_CXX_COMPILER",
+ "value": "/usr/bin/g++-12",
+ "type": "FILEPATH"
+ }
+ ],
+ "remoteMachineName": "${defaultRemoteMachineName}",
+ "remoteCMakeListsRoot": "$HOME/.vs/${projectDirName}/${workspaceHash}/src",
+ "remoteBuildRoot": "$HOME/.vs/${projectDirName}/${workspaceHash}/out/build/${name}",
+ "remoteInstallRoot": "$HOME/.vs/${projectDirName}/${workspaceHash}/out/install/${name}",
+ "remoteCopySources": true,
+ "rsyncCommandArgs": "-t --delete --delete-excluded",
+ "remoteCopyBuildOutput": false,
+ "remoteCopySourcesMethod": "rsync"
+ },
+ {
+ "name": "x64-Debug-v142",
+ "generator": "Visual Studio 17 2022 Win64",
+ "configurationType": "Debug",
+ "buildRoot": "${projectDir}\\out\\build\\${name}",
+ "installRoot": "${projectDir}\\out\\install\\${name}",
+ "cmakeCommandArgs": "-Tv142",
+ "buildCommandArgs": "",
+ "ctestCommandArgs": "",
+ "inheritEnvironments": [ "msvc_x64_x64" ],
+ "variables": [
+ {
+ "name": "CMAKE_EXPORT_COMPILE_COMMANDS",
+ "value": "True",
+ "type": "BOOL"
+ }
+ ]
+ }
+ ]
}
\ No newline at end of file
diff --git a/Folder.DotSettings b/Folder.DotSettings
index 90dda5ce4..b384d7f5c 100644
--- a/Folder.DotSettings
+++ b/Folder.DotSettings
@@ -1,6 +1,8 @@
True
True
+ SUGGESTION
+ SUGGESTION
HINT
HINT
SUGGESTION
@@ -98,4 +100,5 @@ Distributed under the Boost Software License, Version 1.0.
True
True
True
+ True
diff --git a/benchmarks/CMakeLists.txt b/benchmarks/CMakeLists.txt
new file mode 100644
index 000000000..2ee0fa991
--- /dev/null
+++ b/benchmarks/CMakeLists.txt
@@ -0,0 +1,29 @@
+CPMAddPackage("gh:catchorg/Catch2@3.4.0")
+include("${Catch2_SOURCE_DIR}/extras/Catch.cmake")
+
+CPMAddPackage("gh:fmtlib/fmt#10.1.1")
+CPMAddPackage("gh:ericniebler/range-v3#0.12.0")
+CPMAddPackage("gh:kokkos/mdspan#mdspan-0.6.0")
+CPMAddPackage(
+ name boost
+ URL "https://github.com/boostorg/boost/releases/download/boost-1.83.0/boost-1.83.0.zip"
+)
+
+add_executable(
+ Simple-Utility-Benchmarks
+)
+
+add_subdirectory("graph")
+
+target_link_libraries(
+ Simple-Utility-Benchmarks
+ PRIVATE
+ Simple::Utility
+ Catch2::Catch2WithMain
+ std::mdspan
+ Boost::graph
+ fmt::fmt
+ range-v3::range-v3
+)
+
+catch_discover_tests(Simple-Utility-Benchmarks)
diff --git a/benchmarks/Defines.hpp b/benchmarks/Defines.hpp
new file mode 100644
index 000000000..166d3781d
--- /dev/null
+++ b/benchmarks/Defines.hpp
@@ -0,0 +1,19 @@
+// Copyright Dominic Koepke 2019 - 2023.
+// Distributed under the Boost Software License, Version 1.0.
+// (See accompanying file LICENSE_1_0.txt or copy at
+// https://www.boost.org/LICENSE_1_0.txt)
+
+#ifndef SIMPLE_UTILITY_BENCHMARKS_DEFINES_HPP
+#define SIMPLE_UTILITY_BENCHMARKS_DEFINES_HPP
+
+#pragma once
+
+#include
+
+[[nodiscard]]
+inline std::filesystem::path artifacts_root_path()
+{
+ return std::filesystem::current_path() / "artifacts";
+}
+
+#endif
diff --git a/benchmarks/graph/AStarBoostComparison.cpp b/benchmarks/graph/AStarBoostComparison.cpp
new file mode 100644
index 000000000..e2be92395
--- /dev/null
+++ b/benchmarks/graph/AStarBoostComparison.cpp
@@ -0,0 +1,469 @@
+// Copyright Dominic Koepke 2019 - 2023.
+// Distributed under the Boost Software License, Version 1.0.
+// (See accompanying file LICENSE_1_0.txt or copy at
+// https://www.boost.org/LICENSE_1_0.txt)
+
+#include
+
+#include
+#include
+#include
+#include
+#include
+
+#include
+#include
+#include
+
+#include
+#include
+
+#include
+#include
+#include
+#include
+
+#include "../Defines.hpp"
+
+#include "Simple-Utility/graph/AStarSearch.hpp"
+#include "Simple-Utility/graph/mixins/graph/std_reference_wrapper.hpp"
+
+/* Most of this code is directly taken from https://github.com/boostorg/graph/blob/develop/example/astar_maze.cpp
+ * and slightly modernized.
+ * The example types are used for benchmarking both, boost::graph and sl::graph.
+ */
+
+// Distance traveled in the maze
+using distance = double;
+
+using grid = boost::grid_graph<2>;
+using vertex_descriptor = boost::graph_traits::vertex_descriptor;
+using edge_descriptor = boost::graph_traits::edge_descriptor;
+using vertices_size_type = boost::graph_traits::vertices_size_type;
+
+struct vertex_hash
+{
+ using argument_type = vertex_descriptor;
+ using result_type = std::size_t;
+
+ std::size_t operator()(const vertex_descriptor& u) const
+ {
+ std::size_t seed = 0;
+ boost::hash_combine(seed, u[0]);
+ boost::hash_combine(seed, u[1]);
+ return seed;
+ }
+};
+
+using vertex_set = boost::unordered_set;
+using filtered_grid = boost::vertex_subset_complement_filter::type;
+
+// A searchable maze
+//
+// The maze is grid of locations which can either be empty or contain a
+// barrier. You can move to an adjacent location in the grid by going up,
+// down, left and right. Moving onto a barrier is not allowed. The maze can
+// be solved by finding a path from the lower-left-hand corner to the
+// upper-right-hand corner. If no open path exists between these two
+// locations, the maze is unsolvable.
+//
+// The maze is implemented as a filtered grid graph where locations are
+// vertices. Barrier vertices are filtered out of the graph.
+//
+// A-star search is used to find a path through the maze. Each edge has a
+// weight of one, so the total path length is equal to the number of edges
+// traversed.
+class maze
+{
+public:
+ friend std::ostream& operator<<(std::ostream&, const maze&);
+ friend void random_maze(maze&, std::uint32_t);
+
+ explicit maze(const std::size_t x, const std::size_t y)
+ : m_Grid{grid::vertex_descriptor{x, y}},
+ m_BarrierGrid(make_vertex_subset_complement_filter(m_Grid, m_Barriers))
+ {
+ }
+
+ // The length of the maze along the specified dimension.
+ vertices_size_type length(std::size_t d) const { return m_Grid.length(d); }
+
+ bool has_barrier(vertex_descriptor u) const
+ {
+ return m_Barriers.find(u) != m_Barriers.end();
+ }
+
+ // Try to find a path from the lower-left-hand corner source (0,0) to the
+ // upper-right-hand corner goal (x-1, y-1).
+ vertex_descriptor source() const { return vertex(0, m_Grid); }
+
+ vertex_descriptor goal() const
+ {
+ return vertex(num_vertices(m_Grid) - 1, m_Grid);
+ }
+
+ std::optional> solve();
+
+ bool solved() const { return !m_Solution.empty(); }
+
+ bool solution_contains(vertex_descriptor u) const
+ {
+ return m_Solution.find(u) != m_Solution.end();
+ }
+
+ const filtered_grid& get_grid() const noexcept
+ {
+ return m_BarrierGrid;
+ }
+
+private:
+ // The grid underlying the maze
+ grid m_Grid;
+ // The barriers in the maze
+ vertex_set m_Barriers{};
+ // The underlying maze grid with barrier vertices filtered out
+ filtered_grid m_BarrierGrid;
+ // The vertices on a solution path through the maze
+ vertex_set m_Solution{};
+ // The length of the solution path
+ distance m_SolutionLength{};
+};
+
+// Euclidean heuristic for a grid
+//
+// This calculates the Euclidean distance between a vertex and a goal
+// vertex.
+class euclidean_heuristic
+ : public boost::astar_heuristic
+{
+public:
+ explicit euclidean_heuristic(const vertex_descriptor& goal)
+ : m_Goal{goal}
+ {
+ }
+
+ double operator()(vertex_descriptor v) const
+ {
+ return std::sqrt(
+ std::pow(static_cast(m_Goal[0] - v[0]), 2)
+ + std::pow(static_cast(m_Goal[1] - v[1]), 2));
+ }
+
+private:
+ vertex_descriptor m_Goal;
+};
+
+// Exception thrown when the goal vertex is found
+struct found_goal
+{
+};
+
+// Visitor that terminates when we find the goal vertex
+struct astar_goal_visitor : public boost::default_astar_visitor
+{
+ explicit astar_goal_visitor(const vertex_descriptor& goal)
+ : m_Goal{goal}
+ {
+ }
+
+ void examine_vertex(const vertex_descriptor& u, const filtered_grid&) const
+ {
+ if (u == m_Goal)
+ {
+ throw found_goal{};
+ }
+ }
+
+private:
+ vertex_descriptor m_Goal;
+};
+
+// Solve the maze using A-star search. Return true if a solution was found.
+std::optional> maze::solve()
+{
+ // The predecessor map is a vertex-to-vertex mapping.
+ std::unordered_map predecessors{};
+ // The distance map is a vertex-to-distance mapping.
+ std::unordered_map distances{};
+
+ const vertex_descriptor g = goal();
+ const vertex_descriptor s = source();
+
+ try
+ {
+ astar_search(
+ m_BarrierGrid,
+ source(),
+ euclidean_heuristic{g},
+ weight_map(boost::static_property_map{distance{1}})
+ .predecessor_map(boost::associative_property_map{predecessors})
+ .distance_map(boost::associative_property_map{distances})
+ .visitor(astar_goal_visitor{g}));
+ }
+ catch (const found_goal&)
+ {
+ // Walk backwards from the goal through the predecessor chain adding
+ // vertices to the solution path.
+ for (vertex_descriptor u = g; u != s; u = predecessors[u])
+ {
+ m_Solution.insert(u);
+ }
+ m_Solution.insert(s);
+ m_SolutionLength = distances[g];
+ return std::tuple{m_Solution, m_SolutionLength};
+ }
+
+ return std::nullopt;
+}
+
+// Generate a maze with a random assignment of barriers.
+void random_maze(maze& m, const std::uint32_t seed)
+{
+ const vertices_size_type n = num_vertices(m.m_Grid);
+ const vertex_descriptor s = m.source();
+ const vertex_descriptor g = m.goal();
+ // One quarter of the cells in the maze should be barriers.
+ vertices_size_type barriers{n / 4u};
+
+ std::mt19937 rng{seed};
+ while (barriers > 0)
+ {
+ // Choose horizontal or vertical direction.
+ const std::size_t direction = std::uniform_int_distribution{0, 1}(rng);
+ // Walls range up to one quarter the dimension length in this direction.
+ vertices_size_type wall = std::uniform_int_distribution{1, m.length(direction) / 4}(rng);
+ // Create the wall while decrementing the total barrier count.
+ vertex_descriptor u = vertex(std::uniform_int_distribution{0, n - 1}(rng), m.m_Grid);
+ while (wall)
+ {
+ // Start and goal spaces should never be barriers.
+ if (u != s && u != g)
+ {
+ --wall;
+ if (!m.has_barrier(u))
+ {
+ m.m_Barriers.insert(u);
+ if (0 == --barriers)
+ {
+ break;
+ }
+ }
+ }
+ vertex_descriptor v = m.m_Grid.next(u, direction);
+ // Stop creating this wall if we reached the maze's edge.
+ if (u == v)
+ {
+ break;
+ }
+ u = v;
+ }
+ }
+}
+
+/* #############################
+ * Begin sl::graph related symbols
+ ############################## */
+
+template <>
+struct sl::graph::graph::traits
+{
+ using edge_type = CommonWeightedEdge;
+ using vertex_type = edge::vertex_t;
+ using weight_type = edge::weight_t;
+};
+
+template <>
+struct sl::graph::customize::out_edges_fn
+{
+ using edge_type = graph::edge_t;
+ using vertex_type = edge::vertex_t;
+ using weight_type = edge::weight_t;
+
+ [[nodiscard]]
+ auto operator ()(const maze& m, const vertex_type& current) const
+ {
+ const auto& g = m.get_grid();
+ const auto [edgesBegin, edgesEnd] = out_edges(current, g);
+
+ return ranges::subrange{edgesBegin, edgesEnd}
+ | ranges::views::transform([&](const auto& e) { return edge_type{target(e, g), 1.}; });
+ }
+};
+
+std::optional> sl_graph_solve(const maze& m)
+{
+ namespace sg = sl::graph;
+
+ using Node = sg::decorator::PredecessorNode>;
+ using Stream = sg::astar::Stream<
+ std::reference_wrapper,
+ euclidean_heuristic,
+ Node,
+ sg::tracker::CommonHashMap>;
+
+ Stream stream{
+ m.source(),
+ std::make_tuple(std::ref(m)),
+ std::tuple{},
+ std::tuple{},
+ std::tuple{sg::astar::NodeFactory{euclidean_heuristic{m.goal()}}}
+ };
+
+ std::unordered_map nodes{};
+ for (const auto& node : stream)
+ {
+ nodes.emplace(node.vertex, node);
+ if (node.vertex == m.goal())
+ {
+ break;
+ }
+ }
+
+ if (nodes.contains(m.goal()))
+ {
+ // Walk backwards from the goal through the predecessor chain adding
+ // vertices to the solution path.
+ vertex_set solution{};
+ for (Node current = nodes[m.goal()]; current.predecessor; current = nodes[*current.predecessor])
+ {
+ solution.emplace(current.vertex);
+ }
+ solution.emplace(m.source());
+
+ return std::tuple{solution, nodes[m.goal()].cost};
+ }
+
+ return std::nullopt;
+}
+
+/* #############################
+ * End sl::graph related symbols
+ ############################## */
+
+// Print the maze as an ASCII map.
+std::ostream& operator<<(std::ostream& output, const maze& m)
+{
+ constexpr char barrier = '#';
+ constexpr char start = 'S';
+ constexpr char goal = 'G';
+
+ // Header
+ for (vertices_size_type i = 0; i < m.length(0) + 2; i++)
+ {
+ output << barrier;
+ }
+ output << std::endl;
+ // Body
+ for (vertices_size_type i = 0; i < m.length(1); ++i)
+ {
+ const vertices_size_type y = m.length(1) - 1 - i;
+ // Enumerate rows in reverse order and columns in regular order so that
+ // (0,0) appears in the lower left-hand corner. This requires that y be
+ // int and not the unsigned vertices_size_type because the loop exit
+ // condition is y==-1.
+ for (vertices_size_type x = 0; x < m.length(0); x++)
+ {
+ // Put a barrier on the left-hand side.
+ if (x == 0)
+ {
+ output << barrier;
+ }
+ // Put the character representing this point in the maze grid.
+ vertex_descriptor u = {{x, y}};
+ if (u == m.source())
+ {
+ output << start;
+ }
+ else if (u == m.goal())
+ {
+ output << goal;
+ }
+ else if (m.solution_contains(u))
+ {
+ output << ".";
+ }
+ else if (m.has_barrier(u))
+ {
+ output << barrier;
+ }
+ else
+ {
+ output << " ";
+ }
+ // Put a barrier on the right-hand side.
+ if (x == m.length(0) - 1)
+ {
+ output << barrier;
+ }
+ }
+ // Put a newline after every row except the last one.
+ output << std::endl;
+ }
+ // Footer
+ for (vertices_size_type i = 0; i < m.length(0) + 2; i++)
+ {
+ output << barrier;
+ }
+ if (m.solved())
+ {
+ output << std::endl << "Solution length " << m.m_SolutionLength;
+ }
+ else
+ {
+ output << std::endl << "Not solvable";
+ }
+
+ return output;
+}
+
+TEMPLATE_TEST_CASE_SIG(
+ "Benchmarking sl::graph vs boost::graph AStar.",
+ "[vs_boost][benchmark][benchmark::graph]",
+ ((int width, int height), width, height),
+ (8, 8),
+ (16, 16),
+ (32, 32),
+ (64, 64),
+ (128, 128),
+ (256, 256),
+ (512, 512),
+ (1024, 1024)
+)
+{
+ const std::uint32_t seed = GENERATE(
+ // add static seeds here
+ Catch::getSeed());
+
+ maze m{width, height};
+ random_maze(m, Catch::getSeed());
+
+ SECTION("Compare the results of both implementations.")
+ {
+ const std::optional boostSolution = [m, fileName = fmt::format("./{}_{}x{}.maze.txt", seed, width, height)]() mutable
+ {
+ auto result = m.solve();
+ const auto path = artifacts_root_path() / "graph" / "astar_vs_boost";
+ create_directories(path);
+ std::ofstream out{path / fileName};
+ out << m;
+ return result;
+ }();
+ const std::optional slSolution = [m] { return sl_graph_solve(m); }();
+
+ REQUIRE(boostSolution.has_value() == slSolution.has_value());
+ if (boostSolution.has_value())
+ {
+ REQUIRE(std::get<1>(*boostSolution) == Catch::Approx{std::get<1>(*slSolution)});
+ }
+ }
+
+ BENCHMARK("boost::graph")
+ {
+ return m.solve();
+ };
+
+ BENCHMARK("sl::graph")
+ {
+ return sl_graph_solve(m);
+ };
+}
diff --git a/benchmarks/graph/CMakeLists.txt b/benchmarks/graph/CMakeLists.txt
new file mode 100644
index 000000000..c72da6926
--- /dev/null
+++ b/benchmarks/graph/CMakeLists.txt
@@ -0,0 +1,5 @@
+target_sources(
+ Simple-Utility-Benchmarks
+ PRIVATE
+ "AStarBoostComparison.cpp"
+ )
diff --git a/docs/Doxyfile.in b/docs/Doxyfile.in
index 2e20b2edf..44b0bd84c 100644
--- a/docs/Doxyfile.in
+++ b/docs/Doxyfile.in
@@ -1,4 +1,4 @@
-# Doxyfile 1.9.6
+# Doxyfile 1.9.8
# This file describes the settings to be used by the documentation system
# doxygen (www.doxygen.org) for a project.
@@ -363,6 +363,17 @@ MARKDOWN_SUPPORT = YES
TOC_INCLUDE_HEADINGS = 5
+# The MARKDOWN_ID_STYLE tag can be used to specify the algorithm used to
+# generate identifiers for the Markdown headings. Note: Every identifier is
+# unique.
+# Possible values are: DOXYGEN use a fixed 'autotoc_md' string followed by a
+# sequence number starting at 0 and GITHUB use the lower case version of title
+# with any whitespace replaced by '-' and punctuation characters removed.
+# The default value is: DOXYGEN.
+# This tag requires that the tag MARKDOWN_SUPPORT is set to YES.
+
+MARKDOWN_ID_STYLE = DOXYGEN
+
# When enabled doxygen tries to link words that correspond to documented
# classes, or namespaces to their corresponding documentation. Such a link can
# be prevented in individual cases by putting a % sign in front of the word or
@@ -487,6 +498,14 @@ LOOKUP_CACHE_SIZE = 0
NUM_PROC_THREADS = 1
+# If the TIMESTAMP tag is set different from NO then each generated page will
+# contain the date or date and time when the page was generated. Setting this to
+# NO can help when comparing the output of multiple runs.
+# Possible values are: YES, NO, DATETIME and DATE.
+# The default value is: NO.
+
+TIMESTAMP = NO
+
#---------------------------------------------------------------------------
# Build related configuration options
#---------------------------------------------------------------------------
@@ -872,7 +891,14 @@ WARN_IF_UNDOC_ENUM_VAL = NO
# a warning is encountered. If the WARN_AS_ERROR tag is set to FAIL_ON_WARNINGS
# then doxygen will continue running as if WARN_AS_ERROR tag is set to NO, but
# at the end of the doxygen process doxygen will return with a non-zero status.
-# Possible values are: NO, YES and FAIL_ON_WARNINGS.
+# If the WARN_AS_ERROR tag is set to FAIL_ON_WARNINGS_PRINT then doxygen behaves
+# like FAIL_ON_WARNINGS but in case no WARN_LOGFILE is defined doxygen will not
+# write the warning messages in between other messages but write them at the end
+# of a run, in case a WARN_LOGFILE is defined the warning messages will be
+# besides being in the defined file also be shown at the end of a run, unless
+# the WARN_LOGFILE is defined as - i.e. standard output (stdout) in that case
+# the behavior will remain as with the setting FAIL_ON_WARNINGS.
+# Possible values are: NO, YES, FAIL_ON_WARNINGS and FAIL_ON_WARNINGS_PRINT.
# The default value is: NO.
WARN_AS_ERROR = NO
@@ -951,12 +977,12 @@ INPUT_FILE_ENCODING =
# Note the list of default checked file patterns might differ from the list of
# default file extension mappings.
#
-# If left blank the following patterns are tested:*.c, *.cc, *.cxx, *.cpp,
-# *.c++, *.java, *.ii, *.ixx, *.ipp, *.i++, *.inl, *.idl, *.ddl, *.odl, *.h,
-# *.hh, *.hxx, *.hpp, *.h++, *.l, *.cs, *.d, *.php, *.php4, *.php5, *.phtml,
-# *.inc, *.m, *.markdown, *.md, *.mm, *.dox (to be provided as doxygen C
-# comment), *.py, *.pyw, *.f90, *.f95, *.f03, *.f08, *.f18, *.f, *.for, *.vhd,
-# *.vhdl, *.ucf, *.qsf and *.ice.
+# If left blank the following patterns are tested:*.c, *.cc, *.cxx, *.cxxm,
+# *.cpp, *.cppm, *.c++, *.c++m, *.java, *.ii, *.ixx, *.ipp, *.i++, *.inl, *.idl,
+# *.ddl, *.odl, *.h, *.hh, *.hxx, *.hpp, *.h++, *.ixx, *.l, *.cs, *.d, *.php,
+# *.php4, *.php5, *.phtml, *.inc, *.m, *.markdown, *.md, *.mm, *.dox (to be
+# provided as doxygen C comment), *.py, *.pyw, *.f90, *.f95, *.f03, *.f08,
+# *.f18, *.f, *.for, *.vhd, *.vhdl, *.ucf, *.qsf and *.ice.
FILE_PATTERNS = *.c \
*.cc \
@@ -1042,9 +1068,6 @@ EXCLUDE_PATTERNS =
# output. The symbol name can be a fully qualified name, a word, or if the
# wildcard * is used, a substring. Examples: ANamespace, AClass,
# ANamespace::AClass, ANamespace::*Test
-#
-# Note that the wildcards are matched against the file with absolute path, so to
-# exclude all test directories use the pattern */test/*
EXCLUDE_SYMBOLS = sl:*:detail
@@ -1427,15 +1450,6 @@ HTML_COLORSTYLE_SAT = 100
HTML_COLORSTYLE_GAMMA = 80
-# If the HTML_TIMESTAMP tag is set to YES then the footer of each generated HTML
-# page will contain the date and time when the page was generated. Setting this
-# to YES can help to show when doxygen was last run and thus if the
-# documentation is up to date.
-# The default value is: NO.
-# This tag requires that the tag GENERATE_HTML is set to YES.
-
-HTML_TIMESTAMP = NO
-
# If the HTML_DYNAMIC_MENUS tag is set to YES then the generated HTML
# documentation will contain a main index with vertical navigation menus that
# are dynamically created via JavaScript. If disabled, the navigation index will
@@ -1455,6 +1469,13 @@ HTML_DYNAMIC_MENUS = YES
HTML_DYNAMIC_SECTIONS = NO
+# If the HTML_CODE_FOLDING tag is set to YES then classes and functions can be
+# dynamically folded and expanded in the generated HTML source code.
+# The default value is: YES.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+HTML_CODE_FOLDING = YES
+
# With HTML_INDEX_NUM_ENTRIES one can control the preferred number of entries
# shown in the various tree structured indices initially; the user can expand
# and collapse entries dynamically later on. Doxygen will expand the tree to
@@ -1585,6 +1606,16 @@ BINARY_TOC = NO
TOC_EXPAND = NO
+# The SITEMAP_URL tag is used to specify the full URL of the place where the
+# generated documentation will be placed on the server by the user during the
+# deployment of the documentation. The generated sitemap is called sitemap.xml
+# and placed on the directory specified by HTML_OUTPUT. In case no SITEMAP_URL
+# is specified no sitemap is generated. For information about the sitemap
+# protocol see https://www.sitemaps.org
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+SITEMAP_URL =
+
# If the GENERATE_QHP tag is set to YES and both QHP_NAMESPACE and
# QHP_VIRTUAL_FOLDER are set, an additional index file will be generated that
# can be used as input for Qt's qhelpgenerator to generate a Qt Compressed Help
@@ -2073,9 +2104,16 @@ PDF_HYPERLINKS = YES
USE_PDFLATEX = YES
-# If the LATEX_BATCHMODE tag is set to YES, doxygen will add the \batchmode
-# command to the generated LaTeX files. This will instruct LaTeX to keep running
-# if errors occur, instead of asking the user for help.
+# The LATEX_BATCHMODE tag signals the behavior of LaTeX in case of an error.
+# Possible values are: NO same as ERROR_STOP, YES same as BATCH, BATCH In batch
+# mode nothing is printed on the terminal, errors are scrolled as if is
+# hit at every error; missing files that TeX tries to input or request from
+# keyboard input (\read on a not open input stream) cause the job to abort,
+# NON_STOP In nonstop mode the diagnostic message will appear on the terminal,
+# but there is no possibility of user interaction just like in batch mode,
+# SCROLL In scroll mode, TeX will stop only for missing files to input or if
+# keyboard input is necessary and ERROR_STOP In errorstop mode, TeX will stop at
+# each error, asking for user intervention.
# The default value is: NO.
# This tag requires that the tag GENERATE_LATEX is set to YES.
@@ -2096,14 +2134,6 @@ LATEX_HIDE_INDICES = NO
LATEX_BIB_STYLE = plain
-# If the LATEX_TIMESTAMP tag is set to YES then the footer of each generated
-# page will contain the date and time when the page was generated. Setting this
-# to NO can help when comparing the output of multiple runs.
-# The default value is: NO.
-# This tag requires that the tag GENERATE_LATEX is set to YES.
-
-LATEX_TIMESTAMP = NO
-
# The LATEX_EMOJI_DIRECTORY tag is used to specify the (relative or absolute)
# path from which the emoji images will be read. If a relative path is entered,
# it will be relative to the LATEX_OUTPUT directory. If left blank the
@@ -2269,7 +2299,7 @@ DOCBOOK_OUTPUT = docbook
#---------------------------------------------------------------------------
# If the GENERATE_AUTOGEN_DEF tag is set to YES, doxygen will generate an
-# AutoGen Definitions (see http://autogen.sourceforge.net/) file that captures
+# AutoGen Definitions (see https://autogen.sourceforge.net/) file that captures
# the structure of the code including all documentation. Note that this feature
# is still experimental and incomplete at the moment.
# The default value is: NO.
@@ -2280,6 +2310,28 @@ GENERATE_AUTOGEN_DEF = NO
# Configuration options related to Sqlite3 output
#---------------------------------------------------------------------------
+# If the GENERATE_SQLITE3 tag is set to YES doxygen will generate a Sqlite3
+# database with symbols found by doxygen stored in tables.
+# The default value is: NO.
+
+GENERATE_SQLITE3 = NO
+
+# The SQLITE3_OUTPUT tag is used to specify where the Sqlite3 database will be
+# put. If a relative path is entered the value of OUTPUT_DIRECTORY will be put
+# in front of it.
+# The default directory is: sqlite3.
+# This tag requires that the tag GENERATE_SQLITE3 is set to YES.
+
+SQLITE3_OUTPUT = sqlite3
+
+# The SQLITE3_OVERWRITE_DB tag is set to YES, the existing doxygen_sqlite3.db
+# database file will be recreated with each doxygen run. If set to NO, doxygen
+# will warn if an a database file is already found and not modify it.
+# The default value is: YES.
+# This tag requires that the tag GENERATE_SQLITE3 is set to YES.
+
+SQLITE3_RECREATE_DB = YES
+
#---------------------------------------------------------------------------
# Configuration options related to the Perl module output
#---------------------------------------------------------------------------
@@ -2422,15 +2474,15 @@ TAGFILES =
GENERATE_TAGFILE =
-# If the ALLEXTERNALS tag is set to YES, all external class will be listed in
-# the class index. If set to NO, only the inherited external classes will be
-# listed.
+# If the ALLEXTERNALS tag is set to YES, all external classes and namespaces
+# will be listed in the class and namespace index. If set to NO, only the
+# inherited external classes will be listed.
# The default value is: NO.
ALLEXTERNALS = NO
# If the EXTERNAL_GROUPS tag is set to YES, all external groups will be listed
-# in the modules index. If set to NO, only the current project's groups will be
+# in the topic index. If set to NO, only the current project's groups will be
# listed.
# The default value is: YES.
@@ -2444,16 +2496,9 @@ EXTERNAL_GROUPS = YES
EXTERNAL_PAGES = YES
#---------------------------------------------------------------------------
-# Configuration options related to the dot tool
+# Configuration options related to diagram generator tools
#---------------------------------------------------------------------------
-# You can include diagrams made with dia in doxygen documentation. Doxygen will
-# then run dia to produce the diagram and insert it in the documentation. The
-# DIA_PATH tag allows you to specify the directory where the dia binary resides.
-# If left empty dia is assumed to be found in the default search path.
-
-DIA_PATH =
-
# If set to YES the inheritance and collaboration graphs will hide inheritance
# and usage relations if the target is undocumented or is not a class.
# The default value is: YES.
@@ -2462,7 +2507,7 @@ HIDE_UNDOC_RELATIONS = YES
# If you set the HAVE_DOT tag to YES then doxygen will assume the dot tool is
# available from the path. This tool is part of Graphviz (see:
-# http://www.graphviz.org/), a graph visualization toolkit from AT&T and Lucent
+# https://www.graphviz.org/), a graph visualization toolkit from AT&T and Lucent
# Bell Labs. The other options in this section have no effect if this option is
# set to NO
# The default value is: NO.
@@ -2515,13 +2560,15 @@ DOT_NODE_ATTR = "shape=box,height=0.2,width=0.4"
DOT_FONTPATH =
-# If the CLASS_GRAPH tag is set to YES (or GRAPH) then doxygen will generate a
-# graph for each documented class showing the direct and indirect inheritance
-# relations. In case HAVE_DOT is set as well dot will be used to draw the graph,
-# otherwise the built-in generator will be used. If the CLASS_GRAPH tag is set
-# to TEXT the direct and indirect inheritance relations will be shown as texts /
-# links.
-# Possible values are: NO, YES, TEXT and GRAPH.
+# If the CLASS_GRAPH tag is set to YES or GRAPH or BUILTIN then doxygen will
+# generate a graph for each documented class showing the direct and indirect
+# inheritance relations. In case the CLASS_GRAPH tag is set to YES or GRAPH and
+# HAVE_DOT is enabled as well, then dot will be used to draw the graph. In case
+# the CLASS_GRAPH tag is set to YES and HAVE_DOT is disabled or if the
+# CLASS_GRAPH tag is set to BUILTIN, then the built-in generator will be used.
+# If the CLASS_GRAPH tag is set to TEXT the direct and indirect inheritance
+# relations will be shown as texts / links.
+# Possible values are: NO, YES, TEXT, GRAPH and BUILTIN.
# The default value is: YES.
CLASS_GRAPH = YES
@@ -2529,15 +2576,21 @@ CLASS_GRAPH = YES
# If the COLLABORATION_GRAPH tag is set to YES then doxygen will generate a
# graph for each documented class showing the direct and indirect implementation
# dependencies (inheritance, containment, and class references variables) of the
-# class with other documented classes.
+# class with other documented classes. Explicit enabling a collaboration graph,
+# when COLLABORATION_GRAPH is set to NO, can be accomplished by means of the
+# command \collaborationgraph. Disabling a collaboration graph can be
+# accomplished by means of the command \hidecollaborationgraph.
# The default value is: YES.
# This tag requires that the tag HAVE_DOT is set to YES.
COLLABORATION_GRAPH = YES
# If the GROUP_GRAPHS tag is set to YES then doxygen will generate a graph for
-# groups, showing the direct groups dependencies. See also the chapter Grouping
-# in the manual.
+# groups, showing the direct groups dependencies. Explicit enabling a group
+# dependency graph, when GROUP_GRAPHS is set to NO, can be accomplished by means
+# of the command \groupgraph. Disabling a directory graph can be accomplished by
+# means of the command \hidegroupgraph. See also the chapter Grouping in the
+# manual.
# The default value is: YES.
# This tag requires that the tag HAVE_DOT is set to YES.
@@ -2597,7 +2650,9 @@ TEMPLATE_RELATIONS = YES
# If the INCLUDE_GRAPH, ENABLE_PREPROCESSING and SEARCH_INCLUDES tags are set to
# YES then doxygen will generate a graph for each documented file showing the
# direct and indirect include dependencies of the file with other documented
-# files.
+# files. Explicit enabling an include graph, when INCLUDE_GRAPH is is set to NO,
+# can be accomplished by means of the command \includegraph. Disabling an
+# include graph can be accomplished by means of the command \hideincludegraph.
# The default value is: YES.
# This tag requires that the tag HAVE_DOT is set to YES.
@@ -2606,7 +2661,10 @@ INCLUDE_GRAPH = YES
# If the INCLUDED_BY_GRAPH, ENABLE_PREPROCESSING and SEARCH_INCLUDES tags are
# set to YES then doxygen will generate a graph for each documented file showing
# the direct and indirect include dependencies of the file with other documented
-# files.
+# files. Explicit enabling an included by graph, when INCLUDED_BY_GRAPH is set
+# to NO, can be accomplished by means of the command \includedbygraph. Disabling
+# an included by graph can be accomplished by means of the command
+# \hideincludedbygraph.
# The default value is: YES.
# This tag requires that the tag HAVE_DOT is set to YES.
@@ -2646,7 +2704,10 @@ GRAPHICAL_HIERARCHY = YES
# If the DIRECTORY_GRAPH tag is set to YES then doxygen will show the
# dependencies a directory has on other directories in a graphical way. The
# dependency relations are determined by the #include relations between the
-# files in the directories.
+# files in the directories. Explicit enabling a directory graph, when
+# DIRECTORY_GRAPH is set to NO, can be accomplished by means of the command
+# \directorygraph. Disabling a directory graph can be accomplished by means of
+# the command \hidedirectorygraph.
# The default value is: YES.
# This tag requires that the tag HAVE_DOT is set to YES.
@@ -2662,7 +2723,7 @@ DIR_GRAPH_MAX_DEPTH = 1
# The DOT_IMAGE_FORMAT tag can be used to set the image format of the images
# generated by dot. For an explanation of the image formats see the section
# output formats in the documentation of the dot tool (Graphviz (see:
-# http://www.graphviz.org/)).
+# https://www.graphviz.org/)).
# Note: If you choose svg you need to set HTML_FILE_EXTENSION to xhtml in order
# to make the SVG files visible in IE 9+ (other browsers do not have this
# requirement).
@@ -2699,11 +2760,12 @@ DOT_PATH = @DOXY_DOT_PATH@
DOTFILE_DIRS =
-# The MSCFILE_DIRS tag can be used to specify one or more directories that
-# contain msc files that are included in the documentation (see the \mscfile
-# command).
+# You can include diagrams made with dia in doxygen documentation. Doxygen will
+# then run dia to produce the diagram and insert it in the documentation. The
+# DIA_PATH tag allows you to specify the directory where the dia binary resides.
+# If left empty dia is assumed to be found in the default search path.
-MSCFILE_DIRS =
+DIA_PATH =
# The DIAFILE_DIRS tag can be used to specify one or more directories that
# contain dia files that are included in the documentation (see the \diafile
@@ -2780,3 +2842,19 @@ GENERATE_LEGEND = YES
# The default value is: YES.
DOT_CLEANUP = YES
+
+# You can define message sequence charts within doxygen comments using the \msc
+# command. If the MSCGEN_TOOL tag is left empty (the default), then doxygen will
+# use a built-in version of mscgen tool to produce the charts. Alternatively,
+# the MSCGEN_TOOL tag can also specify the name an external tool. For instance,
+# specifying prog as the value, doxygen will call the tool as prog -T
+# -o . The external tool should support
+# output file formats "png", "eps", "svg", and "ismap".
+
+MSCGEN_TOOL =
+
+# The MSCFILE_DIRS tag can be used to specify one or more directories that
+# contain msc files that are included in the documentation (see the \mscfile
+# command).
+
+MSCFILE_DIRS =
diff --git a/include/Simple-Utility/Config.hpp b/include/Simple-Utility/Config.hpp
index 4828bd82b..82885bd1a 100644
--- a/include/Simple-Utility/Config.hpp
+++ b/include/Simple-Utility/Config.hpp
@@ -30,4 +30,16 @@
"." SL_UTILITY_XSTR(SL_UTILITY_VERSION_MINOR) \
"." SL_UTILITY_XSTR(SL_UTILITY_VERSION_PATCH)
+#include
+
+#if defined(_MSC_VER) || (__cpp_lib_format >= 201907L)
+ #define SL_UTILITY_HAS_STD_FORMAT
+#endif
+
+#if defined(_MSC_VER) || (__cpp_lib_ranges >= 202110L)
+ #if not defined(__clang__) || __clang_major__ >= 16L
+ #define SL_UTILITY_HAS_RANGES_VIEWS
+ #endif
+#endif
+
#endif
diff --git a/include/Simple-Utility/Utility.hpp b/include/Simple-Utility/Utility.hpp
new file mode 100644
index 000000000..26cd0cc72
--- /dev/null
+++ b/include/Simple-Utility/Utility.hpp
@@ -0,0 +1,115 @@
+// Copyright Dominic Koepke 2019 - 2023.
+// Distributed under the Boost Software License, Version 1.0.
+// (See accompanying file LICENSE_1_0.txt or copy at
+// https://www.boost.org/LICENSE_1_0.txt)
+
+#ifndef SIMPLE_UTILITY_UTILITY_HPP
+#define SIMPLE_UTILITY_UTILITY_HPP
+
+#pragma once
+
+#include "Simple-Utility/concepts/stl_extensions.hpp"
+
+#include
+#include
+#include
+
+namespace sl
+{
+ /**
+ * \defgroup GROUP_PRIORITY_TAG priority_tag
+ * \brief Helper type, useful when prioritize overloads of an overload set.
+ * \see Got the idea from here: https://quuxplusone.github.io/blog/2021/07/09/priority-tag/
+ * \{
+ */
+
+ /**
+ * \brief Primary template, inheriting from the other specializations with lesser priority.
+ */
+ template
+ struct priority_tag
+ /** \cond */
+ : public priority_tag
+ /** \endcond */
+ {
+ };
+
+ /**
+ * \brief Specialization, denoting the least likely alternative.
+ */
+ template <>
+ struct priority_tag<0>
+ {
+ };
+
+ /**
+ * \}
+ */
+
+ /**
+ * \defgroup GROUP_IN_PLACE in_place
+ * \brief Provides convenient option, to construct arguments in-place.
+ * \{
+ */
+
+ /**
+ * \brief Helper type, enabling deferred construction, which will then (thanks to copy-elision) in-place construct the ``Type`` instance.
+ * \tparam Type The type to be constructed.
+ * \tparam Args The constructor argument types.
+ * \param args The constructor arguments.
+ * \note Instances of this type are not intended to be materialized anywhere. They should only be used during one expression.
+ */
+ template
+ requires std::constructible_from
+ struct in_place_constructor
+ {
+ public:
+ in_place_constructor(const in_place_constructor&) = delete;
+ in_place_constructor& operator =(const in_place_constructor&) = delete;
+ in_place_constructor(in_place_constructor&&) = delete;
+ in_place_constructor& operator =(in_place_constructor&&) = delete;
+
+ ~in_place_constructor() = default;
+
+ [[nodiscard]]
+ in_place_constructor() = default;
+
+ [[nodiscard]]
+ explicit constexpr operator Type() && noexcept(std::is_nothrow_constructible_v)
+ {
+ return std::make_from_tuple(std::move(m_Args));
+ }
+
+ template
+ friend constexpr in_place_constructor in_place(Ts&&... args) noexcept;
+
+ private:
+ std::tuple m_Args;
+
+ [[nodiscard]]
+ explicit constexpr in_place_constructor(std::tuple&& args) noexcept
+ : m_Args{std::move(args)}
+ {
+ }
+ };
+
+ /**
+ * \brief Forwards the given arguments as tuple into the internal storage.
+ * \tparam Type The type to be constructed.
+ * \tparam Args The constructor argument types.
+ * \param args The constructor arguments.
+ * \return in_place_constructor instance, storing the arguments as forwarding references.
+ */
+ template
+ [[nodiscard]]
+ constexpr in_place_constructor in_place(Args&&... args) noexcept
+ {
+ return in_place_constructor{std::forward_as_tuple(std::forward(args)...)};
+ }
+
+ /**
+ * \}
+ */
+}
+
+#endif
diff --git a/include/Simple-Utility/concepts/stl_extensions.hpp b/include/Simple-Utility/concepts/stl_extensions.hpp
index a542f3e7f..90becb299 100644
--- a/include/Simple-Utility/concepts/stl_extensions.hpp
+++ b/include/Simple-Utility/concepts/stl_extensions.hpp
@@ -8,8 +8,11 @@
#pragma once
+#include "Simple-Utility/Config.hpp"
+
#include
#include
+#include
// ReSharper disable CppClangTidyClangDiagnosticDocumentation
// ReSharper disable CppIdenticalOperandsInBinaryExpression
@@ -68,6 +71,15 @@ namespace sl::concepts
template
concept not_same_as = !std::same_as;
+ /**
+ * \brief Checks whether T is not ``void``.
+ * \details This is the inverted counterpart of ``std::is_void_v`` trait.
+ * \see https://en.cppreference.com/w/cpp/types/is_void
+ * \tparam T Type to check.
+ */
+ template
+ concept not_void = !std::is_void_v;
+
/**
* \brief Checks whether the target type is constructible from the source type.
* \details This is the symmetrical counterpart of ``std::constructible_from`` concept with a single constructor argument.
@@ -271,4 +283,49 @@ namespace sl::concepts
*/
}
+#ifdef SL_UTILITY_HAS_STD_FORMAT
+
+#include
+#include
+
+namespace sl::concepts
+{
+ /**
+ * \addtogroup GROUP_STL_EXTENSION_CONCEPTS
+ * \{
+ */
+
+ /**
+ * \brief Determines, whether a complete specialization of ``std::formatter`` for the given (possibly cv-ref qualified) type exists.
+ * \tparam T Type to check.
+ * \tparam Char Used character type.
+ * \details This is an adapted implementation of the ``std::formattable`` concept, which is added c++23.
+ * \note This implementation takes a simple but reasonable shortcut in assuming, that ```Char`` is either ``char`` or ``wchar_t``,
+ * which must not necessarily true.
+ * \see Adapted from here: https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2022/p2286r8.html#concept-formattable
+ * \see https://en.cppreference.com/w/cpp/utility/format/formattable
+ */
+ template
+ concept formattable =
+ std::semiregular, Char>>
+ && requires(
+ std::formatter, Char> formatter,
+ T t,
+ std::conditional_t, std::format_context, std::wformat_context> formatContext,
+ std::basic_format_parse_context parseContext
+ )
+ {
+ { formatter.parse(parseContext) } -> std::same_as::iterator>;
+ {
+ std::as_const(formatter).format(t, formatContext)
+ } -> std::same_as::iterator>;
+ };
+
+ /**
+ * \}
+ */
+}
+
+#endif
+
#endif
diff --git a/include/Simple-Utility/graph/AStarSearch.hpp b/include/Simple-Utility/graph/AStarSearch.hpp
new file mode 100644
index 000000000..85f3f2bfb
--- /dev/null
+++ b/include/Simple-Utility/graph/AStarSearch.hpp
@@ -0,0 +1,184 @@
+// Copyright Dominic Koepke 2019 - 2023.
+// Distributed under the Boost Software License, Version 1.0.
+// (See accompanying file LICENSE_1_0.txt or copy at
+// https://www.boost.org/LICENSE_1_0.txt)
+
+#ifndef SIMPLE_UTILITY_GRAPH_A_STAR_SEARCH_HPP
+#define SIMPLE_UTILITY_GRAPH_A_STAR_SEARCH_HPP
+
+#pragma once
+
+#include "Simple-Utility/graph/Traverse.hpp"
+#include "Simple-Utility/graph/mixins/queue/std_priority_queue.hpp"
+#include "Simple-Utility/graph/mixins/tracker/std_unordered_map.hpp"
+
+namespace sl::graph::concepts
+{
+ template
+ concept heuristic_for = ranked_node
+ && sl::concepts::unqualified
+ && std::movable
+ && std::invocable>
+ && std::convertible_to>, node::rank_t>;
+}
+
+namespace sl::graph::astar::detail
+{
+ template
+ constexpr void check_bounds(const Rank& base, const Rank& increase) noexcept
+ {
+ assert(0 <= base && "base must be non-negative.");
+ assert(0 <= increase && "increase must be non-negative.");
+ assert(increase <= std::numeric_limits::max() - base && "Rank is about to overflow.");
+ }
+}
+
+namespace sl::graph::astar
+{
+ template
+ struct CommonNode
+ {
+ using vertex_type = Vertex;
+ using rank_type = Rank;
+
+ vertex_type vertex{};
+ rank_type cost{};
+ rank_type estimatedPendingCost{};
+
+ [[nodiscard]]
+ friend constexpr rank_type rank(const CommonNode& node) noexcept(noexcept(node.cost + node.cost))
+ {
+ detail::check_bounds(node.cost, node.estimatedPendingCost);
+
+ return node.cost + node.estimatedPendingCost;
+ }
+
+ [[nodiscard]]
+ friend bool operator==(const CommonNode&, const CommonNode&) = default;
+ };
+
+ template Heuristic>
+ struct NodeFactory;
+
+ template > Heuristic>
+ struct NodeFactory, Heuristic>
+ {
+ public:
+ using node_type = CommonNode;
+ using vertex_type = Vertex;
+ using rank_type = Rank;
+
+ [[nodiscard]]
+ explicit NodeFactory(Heuristic heuristic) noexcept(std::is_nothrow_move_constructible_v)
+ : m_Heuristic{std::move(heuristic)}
+ {
+ }
+
+ [[nodiscard]]
+ constexpr node_type operator ()(const vertex_type& origin) const
+ {
+ node_type node{
+ .vertex = origin,
+ .cost = {0},
+ .estimatedPendingCost = std::invoke(m_Heuristic, origin)
+ };
+ detail::check_bounds(node.cost, node.estimatedPendingCost);
+
+ return node;
+ }
+
+ template Edge>
+ [[nodiscard]]
+ constexpr node_type operator ()(const node_type& current, const Edge& edge) const
+ {
+ detail::check_bounds(current.cost, edge::weight(edge));
+
+ node_type node{
+ .vertex = edge::destination(edge),
+ .cost = current.cost + edge::weight(edge),
+ .estimatedPendingCost = std::invoke(m_Heuristic, edge::destination(edge))
+ };
+ detail::check_bounds(node.cost, node.estimatedPendingCost);
+
+ return node;
+ }
+
+ private:
+ SL_UTILITY_NO_UNIQUE_ADDRESS Heuristic m_Heuristic;
+ };
+
+ template
+ struct NodeFactoryTemplate
+ {
+ template
+ using type = NodeFactory;
+ };
+
+ template Heuristic>
+ requires requires { typename decorator::NodeFactory::template type>::node_type; }
+ struct NodeFactory
+ : public decorator::NodeFactory::template type>
+ {
+ private:
+ using Super = decorator::NodeFactory::template type>;
+
+ public:
+ using Super::Super;
+ using Super::operator ();
+ };
+
+ template Strategy>
+ class SingleDestinationHeuristic
+ {
+ public:
+ using vertex_type = Vertex;
+
+ [[nodiscard]]
+ explicit constexpr SingleDestinationHeuristic(
+ Vertex destination
+ ) noexcept(std::is_nothrow_move_constructible_v
+ && std::is_nothrow_default_constructible_v)
+ : m_Destination{std::move(destination)}
+ {
+ }
+
+ [[nodiscard]]
+ explicit constexpr SingleDestinationHeuristic(
+ Vertex destination,
+ Strategy strategy
+ ) noexcept(std::is_nothrow_move_constructible_v
+ && std::is_nothrow_move_constructible_v)
+ : m_Destination{std::move(destination)},
+ m_Strategy{std::move(strategy)}
+ {
+ }
+
+ [[nodiscard]]
+ constexpr auto operator ()(const Vertex& current) const noexcept(std::is_nothrow_invocable_v)
+ {
+ return std::invoke(m_Strategy, m_Destination, current);
+ }
+
+ private:
+ Vertex m_Destination{};
+ Strategy m_Strategy{};
+ };
+
+ template <
+ concepts::basic_graph Graph,
+ typename Heuristic,
+ concepts::ranked_node Node = CommonNode>, edge::weight_t>>,
+ concepts::tracker_for> Tracker = tracker::CommonHashMap>>
+ requires concepts::heuristic_for
+ using Stream = IterableTraverser<
+ sl::graph::detail::BasicTraverser<
+ Node,
+ Graph,
+ queue::CommonPriorityQueue,
+ Tracker,
+ sl::graph::detail::default_explorer_t<
+ Node,
+ NodeFactory>>>;
+}
+
+#endif
diff --git a/include/Simple-Utility/graph/BreadthFirstSearch.hpp b/include/Simple-Utility/graph/BreadthFirstSearch.hpp
new file mode 100644
index 000000000..fc2c52bd4
--- /dev/null
+++ b/include/Simple-Utility/graph/BreadthFirstSearch.hpp
@@ -0,0 +1,40 @@
+// Copyright Dominic Koepke 2019 - 2023.
+// Distributed under the Boost Software License, Version 1.0.
+// (See accompanying file LICENSE_1_0.txt or copy at
+// https://www.boost.org/LICENSE_1_0.txt)
+
+#ifndef SIMPLE_UTILITY_GRAPH_BREADTH_FIRST_SEARCH_HPP
+#define SIMPLE_UTILITY_GRAPH_BREADTH_FIRST_SEARCH_HPP
+
+#pragma once
+
+#include "Simple-Utility/graph/Traverse.hpp"
+#include "Simple-Utility/graph/mixins/queue/std_queue.hpp"
+#include "Simple-Utility/graph/mixins/tracker/std_unordered_map.hpp"
+
+namespace sl::graph::dfs
+{
+ template
+ struct NodeFactory
+ : public detail::NodeFactory
+ {
+ };
+
+ template
+ using CommonNode = CommonBasicNode;
+
+ template <
+ concepts::basic_graph Graph,
+ concepts::basic_node Node = CommonNode>>,
+ concepts::tracker_for> Tracker = tracker::CommonHashMap>>
+ requires (!concepts::ranked_node)
+ using Stream = IterableTraverser<
+ detail::BasicTraverser<
+ Node,
+ Graph,
+ queue::CommonQueue,
+ Tracker,
+ detail::default_explorer_t>>>;
+}
+
+#endif
diff --git a/include/Simple-Utility/graph/Common.hpp b/include/Simple-Utility/graph/Common.hpp
new file mode 100644
index 000000000..ef2bb7420
--- /dev/null
+++ b/include/Simple-Utility/graph/Common.hpp
@@ -0,0 +1,195 @@
+// Copyright Dominic Koepke 2019 - 2023.
+// Distributed under the Boost Software License, Version 1.0.
+// (See accompanying file LICENSE_1_0.txt or copy at
+// https://www.boost.org/LICENSE_1_0.txt)
+
+#ifndef SIMPLE_UTILITY_GRAPH_COMMON_HPP
+#define SIMPLE_UTILITY_GRAPH_COMMON_HPP
+
+#pragma once
+
+#include "Simple-Utility/concepts/operators.hpp"
+#include "Simple-Utility/concepts/stl_extensions.hpp"
+
+#include
+
+/**
+ * \defgroup GROUP_GRAPH graph
+ *
+ * \brief This library offers various graph related algorithms.
+ * \details
+ * # Design philosophy
+ * This library is built completely on concepts, thus almost everything is exchangeable by users. Some parts may be worth the effort, some may not.
+ * Symbols in ``detail`` namespace can be used, but are less secure and may require some more insights into the internals to be used correctly.
+ *
+ * The (as far as I'm aware of) unique core idea of this library is to *somehow* sort the vertices of an arbitrary graph as sequential range. The actual
+ * algorithm then defines the *somehow* in an exchangeable manner.
+ *
+ * Many graph libraries utilize the visitor approach, where the visitor serves as the major customization point and gets notified about pre-defined
+ * events during the whole algorithm runtime. These visitor may or may not be able to abort the algorithm, e.g. if they reached their goal. For example
+ * boost requires the visitor throwing an exception (see [boost graph faq](https://www.boost.org/doc/libs/1_83_0/libs/graph/doc/faq.html)), which is not
+ * what I would consider a clean design.
+ *
+ * The major issue I see with the visitor approach is, that this pollutes the actual algorithm with too much complexity, as it must be versatile enough to
+ * satisfy any (or at least many) user requirements, while optimally disabling the optional features, the user does not need. It's really difficult to think about
+ * any possible use-case the algorithm may be utilized for and then making it generic enough, while still offering top-notch performance, being versatile
+ * and having a clean design.
+ * The iterative approach of this library therefore concentrates on being a cursor into a graph and notifying the user about the currently referred vertex
+ * (including possibly additional information). The users themselves can than decide how to proceed, e.g. if and how to store the yielded state or simply
+ * throwing the whole algorithm state away, because the job is done. Having the algorithm state present as an actual object makes it trivial to run the
+ * algorithm for a specific amount of steps (or until a predicate isn't satisfied, or whatever condition one may come up with) and continue later on.
+ *
+ * Nevertheless, this library neither aims to replace any existing graph library nor does it want to compete with them in performance regards. Other
+ * libraries are (and probably stay) more feature rich, faster and more polished.
+ *
+ * ## Concepts
+ * All algorithms utilize at least some of the general concepts. The term concept doesn't necessarily mean a c++ concept, its rather from a design perspective.
+ * Nevertheless, most of these design concepts are indeed implemented as c++ concepts.
+ *
+ * ### Graph
+ * A graph consists of multiple elements (vertices) and multiple edges. Most of the algorithms allow graphs to be dynamically explored during the traversal,
+ * thus they can be generated on the fly.
+ * For more info see \ref GROUP_GRAPH_GRAPH graph sub-section.
+ *
+ * ### Vertex
+ * A vertex denotes a unique graph element and is used by algorithms to query the graph for further information (e.g. neighboring vertices).
+ *
+ * ### Edges
+ * A edge denotes a connection between two neighboring vertices of a graph and may contain a ``weight``, which is used by some algorithms, to select the best
+ * alternatives out of the known edges.
+ *
+ * ### Node
+ * A node is an info structure generated by traversing algorithms, which is used to keep track of the exploration state. After each exploration step the current
+ * node is returned to the caller, which may cache it in its own container or simply throw it away.
+ *
+ * Nodes consist of at least a vertex, which they are related to, but may also contain additional information provided by the algorithm.
+ * In fact, the algorithms require a minimal property set, but users are free to extend them with their own properties.
+ *
+ * ### Tracker
+ * In general, a Graph may contain loops or merging branches, which will eventually lead to multiple discovery of the same vertices. The trackers task is then
+ * to identify these multiple discoveries and decide if the current vertex should be further investigated or skipped. There are three exploration states, a vertex
+ * can be in:
+ * - ``unknown``: The vertex is not known by the tracker and should be further investigated.
+ * - ``discovered``: The tracker already knows about the vertex, but there may be still a better solution, thus it should be further investigated.
+ * - ``visited``: The vertex has already been visited and should be skipped.
+ *
+ * The actual tracker implementation is usually one of the first optimizations users can apply. Per default, a hash map is used, which is a good general
+ * purpose solution, but isn't the most efficient for many cases. E.g. a grid graph could utilize a 2d bool array, which will then lead to a constant lookup time.
+ *
+ * ### Queue
+ * A queue stores the discovered nodes and returns them back in any order it desires, thus queue doesn't necessarily mean a FIFO structure. In fact, while the breadth-
+ * first-search is indeed built on top of an actual queue, the depth-first-search for example utilizes a stack (LIFO) container. A queue therefore has major influence
+ * about the algorithms behaviour and should therefore only exchanged with an equivalent container.
+ *
+ * ### Traverser
+ * The traverser couples everything together and can be queried for the next vertex, which will then either return the next visited node, or a null-object if there were
+ * no pending nodes left.
+ *
+ * A traverser can be decorated with ``IterableTraverser``, which in fact provides ``begin`` and ``end`` members, and can then be used as a input-range with any existing
+ * algorithms.
+ *
+ * ## Customization points
+ * As already pointed out, the library does utilize concepts very much. But to be fully exchangeable, the library introduces another layer of indirection in the
+ * sub-namespace ``graph:customize``. All templates in this namespace may be specialized for user types and any such specialization will be prioritized by the
+ * library.
+ */
+
+// ReSharper disable CppDoxygenUnresolvedReference
+/**
+ * \defgroup GROUP_GRAPH_CUSTOMIZATION_POINT customization points
+ * \ingroup GROUP_GRAPH
+ *
+ * \brief Customization points offered by the library.
+ * \details The library offers several customization points to the users, which may be used to make custom or third-party types interface compliant.
+ * In fact a special implementation strategy is used, so that each customization point favors the user defined customization point specialization in cases
+ * where an appropriate alternative would exist. All what has to be done is, to introduce a template specialization for the desired entry point. The specialization
+ * must adhere to the expectations of the customization point and should not alter observable behavior in any way.
+ *
+ * ## Example
+ * For example, given the type ``ThirdPartyEdge`` which shall be utilized as an edge type for this library.
+ * \snippet tests/graph/Edge.cpp ThirdPartyEdge definition
+ *
+ * As you can see, it offers a ``vertex`` member function but the library expects a ``destination`` member function instead. Given its a type of a third party you
+ * can not simply alter the function name, nor is it wise to add a free function into a third party namespace. Fortunately the library offers a third option: The
+ * customization points. In this case its ``sl::graph::customize::destination_fn``, which one can specialize here.
+ * \snippet tests/graph/Edge.cpp ThirdPartyEdge destination_fn specialization
+ * And that's it,``ThirdPartyEdge`` can now be used as an edge type in this library.
+ */
+// ReSharper restore CppDoxygenUnresolvedReference
+
+/**
+ * \defgroup GROUP_GRAPH_COMMON_TYPES common types
+ * \ingroup GROUP_GRAPH
+ * \brief Contains common types, which can be utilized for algorithms.
+ * \details The contained types are just default types, which can be utilized for algorithms, but may also be easily exchanged with user types.
+ */
+
+namespace sl::graph::concepts
+{
+ /**
+ * \defgroup GROUP_GRAPH_CONCEPTS concepts
+ * \ingroup GROUP_GRAPH
+ * \brief Contains general library concepts.
+ *\{
+ */
+
+ /**
+ * \brief Checks, whether the given type satisfies the vertex type requirements.
+ * \tparam T Type to check.
+ */
+ template
+ concept vertex = sl::concepts::unqualified
+ && std::equality_comparable
+ && std::copyable;
+
+ /**
+ * \brief Checks, whether the given type satisfies the weight type requirements.
+ * \tparam T Type to check.
+ */
+ template
+ concept weight = sl::concepts::unqualified
+ && std::copyable
+ && sl::concepts::plus
+ && sl::concepts::minus
+ && sl::concepts::plus_assign
+ && sl::concepts::minus_assign;
+
+ /**
+ * \brief Checks, whether the given type satisfies the rank type requirements.
+ * \tparam T Type to check.
+ */
+ template
+ concept rank = sl::concepts::unqualified
+ && std::regular
+ && std::totally_ordered;
+
+ /**
+ * \brief Checks, whether the given type contains a ``vertex_type`` member alias.
+ * \tparam T Type to check.
+ */
+ template
+ concept readable_vertex_type = requires { typename T::vertex_type; }
+ && vertex;
+
+ /**
+ * \brief Checks, whether the given type contains a ``weight_type`` member alias.
+ * \tparam T Type to check.
+ */
+ template
+ concept readable_weight_type = requires { typename T::weight_type; }
+ && weight;
+
+ /**
+ * \brief Checks, whether the given type contains a ``rank_type`` member alias.
+ * \tparam T Type to check.
+ */
+ template
+ concept readable_rank_type = requires { typename T::rank_type; }
+ && rank;
+
+ /**
+ * \}
+ */
+}
+
+#endif
diff --git a/include/Simple-Utility/graph/DepthFirstSearch.hpp b/include/Simple-Utility/graph/DepthFirstSearch.hpp
new file mode 100644
index 000000000..a16ec7ddf
--- /dev/null
+++ b/include/Simple-Utility/graph/DepthFirstSearch.hpp
@@ -0,0 +1,40 @@
+// Copyright Dominic Koepke 2019 - 2023.
+// Distributed under the Boost Software License, Version 1.0.
+// (See accompanying file LICENSE_1_0.txt or copy at
+// https://www.boost.org/LICENSE_1_0.txt)
+
+#ifndef SIMPLE_UTILITY_GRAPH_DEPTH_FIRST_SEARCH_HPP
+#define SIMPLE_UTILITY_GRAPH_DEPTH_FIRST_SEARCH_HPP
+
+#pragma once
+
+#include "Simple-Utility/graph/Traverse.hpp"
+#include "Simple-Utility/graph/mixins/queue/std_stack.hpp"
+#include "Simple-Utility/graph/mixins/tracker/std_unordered_map.hpp"
+
+namespace sl::graph::dfs
+{
+ template
+ struct NodeFactory
+ : public detail::NodeFactory
+ {
+ };
+
+ template
+ using CommonNode = CommonBasicNode;
+
+ template <
+ concepts::basic_graph Graph,
+ concepts::basic_node Node = CommonNode>>,
+ concepts::tracker_for> Tracker = tracker::CommonHashMap>>
+ requires (!concepts::ranked_node)
+ using Stream = IterableTraverser<
+ detail::BasicTraverser<
+ Node,
+ Graph,
+ queue::CommonStack,
+ Tracker,
+ detail::default_explorer_t>>>;
+}
+
+#endif
diff --git a/include/Simple-Utility/graph/Edge.hpp b/include/Simple-Utility/graph/Edge.hpp
new file mode 100644
index 000000000..5d6e36b47
--- /dev/null
+++ b/include/Simple-Utility/graph/Edge.hpp
@@ -0,0 +1,344 @@
+// Copyright Dominic Koepke 2019 - 2023.
+// Distributed under the Boost Software License, Version 1.0.
+// (See accompanying file LICENSE_1_0.txt or copy at
+// https://www.boost.org/LICENSE_1_0.txt)
+
+#ifndef SIMPLE_UTILITY_GRAPH_EDGE_HPP
+#define SIMPLE_UTILITY_GRAPH_EDGE_HPP
+
+#pragma once
+
+#include "Simple-Utility/Utility.hpp"
+#include "Simple-Utility/concepts/stl_extensions.hpp"
+#include "Simple-Utility/graph/Common.hpp"
+
+namespace sl::graph::edge
+{
+ /**
+ * \defgroup GROUP_GRAPH_EDGE edge
+ * \ingroup GROUP_GRAPH
+ * \brief Contains edge related definitions.
+ * \details An edge connects two vertices inside a graph. A minimal edge contains a ``destination`` vertex; a weighted edge contains also a
+ * ``weight`` property.
+ *
+ * \{
+ */
+
+ /**
+ * \brief Primary template is purposely undefined.
+ */
+ template
+ struct traits;
+
+ /**
+ * \brief Convenience alias, exposing the ``vertex_type`` member alias of the \ref sl::graph::edge::traits "traits" type.
+ * \tparam Edge Type to retrieve the info for.
+ */
+ template
+ using vertex_t = typename traits::vertex_type;
+
+ /**
+ * \brief Convenience alias, exposing the ``weight_type`` member alias of the \ref sl::graph::edge::traits "traits" type.
+ * \tparam Edge Type to retrieve the info for.
+ */
+ template
+ using weight_t = typename traits::weight_type;
+
+ /**
+ * \brief General trait specialization for edges, which contain a valid ``vertex_type`` member alias.
+ * \tparam T
+ */
+ template
+ requires concepts::readable_vertex_type
+ struct traits
+ {
+ using vertex_type = typename T::vertex_type;
+ };
+
+ /**
+ * \brief General trait specialization for edges, which contain both, a valid ``vertex_type`` and ``weight_type`` member alias.
+ * \tparam T
+ */
+ template
+ requires concepts::readable_vertex_type
+ && concepts::readable_weight_type
+ struct traits
+ {
+ using vertex_type = typename T::vertex_type;
+ using weight_type = typename T::weight_type;
+ };
+
+ /**
+ * \}
+ */
+}
+
+namespace sl::graph::customize
+{
+ /**
+ * \brief Primary template for the ``weight`` customization point. Is purposely undefined.
+ * \ingroup GROUP_GRAPH_CUSTOMIZATION_POINT_WEIGHT
+ */
+ template
+ struct weight_fn;
+
+ /**
+ * \brief Primary template for the ``destination`` customization point. Is purposely undefined.
+ * \ingroup GROUP_GRAPH_CUSTOMIZATION_POINT_DESTINATION
+ */
+ template
+ struct destination_fn;
+}
+
+namespace sl::graph::edge::detail
+{
+ template
+ requires requires(const Node& node, customize::destination_fn fn)
+ {
+ requires concepts::vertex>;
+ }
+ constexpr decltype(auto) destination(
+ const Node& node,
+ const priority_tag<3>
+ ) noexcept(noexcept(customize::destination_fn{}(node)))
+ {
+ return customize::destination_fn{}(node);
+ }
+
+ template
+ requires requires(const Node& node)
+ {
+ requires concepts::vertex>;
+ }
+ constexpr auto& destination(const Node& node, const priority_tag<2>) noexcept
+ {
+ return node.destination;
+ }
+
+ template
+ requires requires(const Node& node)
+ {
+ requires concepts::vertex>;
+ }
+ constexpr decltype(auto) destination(const Node& node, const priority_tag<1>) noexcept(noexcept(node.destination()))
+ {
+ return node.destination();
+ }
+
+ template
+ requires requires(const Node& node)
+ {
+ requires concepts::vertex>;
+ }
+ constexpr decltype(auto) destination(const Node& node, const priority_tag<0>) noexcept(noexcept(destination(node)))
+ {
+ return destination(node);
+ }
+
+ struct destination_fn
+ {
+ template
+ requires requires(const Node& node, const priority_tag<3> tag)
+ {
+ requires concepts::vertex>;
+ }
+ constexpr decltype(auto) operator ()(const Node& node) const noexcept(noexcept(detail::destination(node, priority_tag<3>{})))
+ {
+ return detail::destination(node, priority_tag<3>{});
+ }
+ };
+
+ template
+ requires requires(const Edge& node, customize::weight_fn fn)
+ {
+ requires concepts::weight>;
+ }
+ constexpr decltype(auto) weight(const Edge& node, const priority_tag<3>) noexcept(noexcept(customize::weight_fn{}(node)))
+ {
+ return customize::weight_fn{}(node);
+ }
+
+ template
+ requires requires(const Edge& node)
+ {
+ requires concepts::weight>;
+ }
+ constexpr auto& weight(const Edge& node, const priority_tag<2>) noexcept
+ {
+ return node.weight;
+ }
+
+ template
+ requires requires(const Edge& node)
+ {
+ requires concepts::weight>;
+ }
+ constexpr decltype(auto) weight(const Edge& node, const priority_tag<1>) noexcept(noexcept(node.weight()))
+ {
+ return node.weight();
+ }
+
+ template
+ requires requires(const Edge& node)
+ {
+ requires concepts::weight>;
+ }
+ constexpr decltype(auto) weight(const Edge& node, const priority_tag<0>) noexcept(noexcept(weight(node)))
+ {
+ return weight(node);
+ }
+
+ struct weight_fn
+ {
+ template
+ requires requires(const Edge& node, const priority_tag<3> tag)
+ {
+ requires concepts::weight>;
+ }
+ constexpr decltype(auto) operator ()(const Edge& node) const noexcept(noexcept(detail::weight(node, priority_tag<3>{})))
+ {
+ return detail::weight(node, priority_tag<3>{});
+ }
+ };
+}
+
+namespace sl::graph::edge
+{
+ /**
+ * \defgroup GROUP_GRAPH_CUSTOMIZATION_POINT_DESTINATION destination
+ * \ingroup GROUP_GRAPH_CUSTOMIZATION_POINT
+ * \ingroup GROUP_GRAPH_EDGE
+ * \brief Queries the edge for its destination vertex.
+ * \details This function internally dispatches the call in regards of the following priority list:
+ * - ``graph::customize::destination_fn`` specialization
+ * - ``destination`` member variable
+ * - ``destination`` member function
+ * - ``destination`` free function (with ADL enabled)
+ *
+ * Specialized ``destination_fn`` should offer an ``operator ()`` definition matching the following signature:
+ * \code{.cpp}
+ * sl::graph::edge::vertex_t operator ()(const Edge&) const;
+ * \endcode
+ * ``Edge`` itself is the user type, for which the entry point is specialized for.
+ *\{
+ */
+
+ /**
+ * \brief Customization point, retrieving the destination vertex of the given edge.
+ */
+ inline constexpr detail::destination_fn destination{};
+
+ /**
+ * \}
+ */
+
+ /**
+ * \defgroup GROUP_GRAPH_CUSTOMIZATION_POINT_WEIGHT weight
+ * \ingroup GROUP_GRAPH_CUSTOMIZATION_POINT
+ * \ingroup GROUP_GRAPH_EDGE
+ * \brief Queries the edge for its weight.
+ * \details This function internally dispatches the call in regards of the following priority list:
+ * - ``graph::customize::weight_fn`` specialization
+ * - ``weight`` member variable
+ * - ``weight`` member function
+ * - ``weight`` free function (with ADL enabled)
+ *
+ * Specialized ``weight_fn`` should offer an ``operator ()`` definition matching the following signature:
+ * \code{.cpp}
+ * sl::graph::edge::weight_t operator ()(const Edge&) const;
+ * \endcode
+ * ``Edge`` itself is the user type, for which the entry point is specialized for.
+ *\{
+ */
+
+ /**
+ * \brief Customization point, retrieving the weight of the given edge.
+ */
+ inline constexpr detail::weight_fn weight{};
+
+ /**
+ * \}
+ */
+}
+
+namespace sl::graph::concepts
+{
+ /**
+ * \brief Determines, whether the given type satisfies the requirements.
+ * \tparam T Type to check.
+ */
+ template
+ concept edge = sl::concepts::unqualified
+ && std::copyable
+ && std::destructible
+ && vertex::vertex_type>
+ && requires(const T& edge)
+ {
+ // fixes compile error on msvc v142
+ // ReSharper disable once CppRedundantTemplateKeyword
+ { edge::destination(edge) } -> std::convertible_to::vertex_type>;
+ };
+
+ /**
+ * \brief Determines, whether the given type satisfies the requirements.
+ * \tparam T Type to check.
+ */
+ template
+ concept weighted_edge = edge
+ && weight::weight_type>
+ && requires(const T& edge)
+ {
+ // fixes compile error on msvc v142
+ // ReSharper disable once CppRedundantTemplateKeyword
+ { edge::weight(edge) } -> std::convertible_to::weight_type>;
+ };
+}
+
+namespace sl::graph
+{
+ /**
+ * \addtogroup GROUP_GRAPH_COMMON_TYPES
+ * \{
+ */
+
+ /**
+ * \brief A basic edge type.
+ * \tparam Vertex The used vertex type.
+ * \note This type is also equality comparable, which is not an actual requirement for basic edge types.
+ */
+ template
+ struct CommonBasicEdge
+ {
+ using vertex_type = Vertex;
+
+ vertex_type destination;
+
+ [[nodiscard]]
+ friend bool operator ==(const CommonBasicEdge&, const CommonBasicEdge&) = default;
+ };
+
+ /**
+ * \brief A weighted edge type.
+ * \tparam Vertex The used vertex type.
+ * \tparam Weight The used weight type.
+ * \note This type is also equality comparable, which is not an actual requirement for weighted edge types.
+ */
+ template
+ struct CommonWeightedEdge
+ {
+ using vertex_type = Vertex;
+ using weight_type = Weight;
+
+ vertex_type destination;
+ weight_type weight;
+
+ [[nodiscard]]
+ friend bool operator==(const CommonWeightedEdge&, const CommonWeightedEdge&) = default;
+ };
+
+ /**
+ * \}
+ */
+}
+
+#endif
diff --git a/include/Simple-Utility/graph/Formatter.hpp b/include/Simple-Utility/graph/Formatter.hpp
new file mode 100644
index 000000000..385c3b031
--- /dev/null
+++ b/include/Simple-Utility/graph/Formatter.hpp
@@ -0,0 +1,163 @@
+// Copyright Dominic Koepke 2019 - 2023.
+// Distributed under the Boost Software License, Version 1.0.
+// (See accompanying file LICENSE_1_0.txt or copy at
+// https://www.boost.org/LICENSE_1_0.txt)
+
+#ifndef SIMPLE_UTILITY_GRAPH_FORMATTER_HPP
+#define SIMPLE_UTILITY_GRAPH_FORMATTER_HPP
+
+#pragma once
+
+#include "Simple-Utility/Config.hpp"
+#include "Simple-Utility/graph/Edge.hpp"
+#include "Simple-Utility/graph/Node.hpp"
+
+#ifdef SL_UTILITY_HAS_STD_FORMAT
+
+#include
+
+namespace sl::graph
+{
+ template
+ requires sl::concepts::formattable, Char>
+ class NodeFormatter
+ {
+ public:
+ static constexpr auto parse(std::basic_format_parse_context& ctx) noexcept
+ {
+ return ctx.begin();
+ }
+
+ template
+ static auto format(const Node& node, FormatContext& ctx)
+ {
+ return std::format_to(ctx.out(), "vertex: {}", node::vertex(node));
+ }
+ };
+
+ template
+ requires sl::concepts::formattable, Char>
+ && sl::concepts::formattable, Char>
+ class NodeFormatter
+ {
+ public:
+ static constexpr auto parse(std::basic_format_parse_context& ctx) noexcept
+ {
+ return ctx.begin();
+ }
+
+ template
+ static auto format(const Node& node, FormatContext& ctx)
+ {
+ return std::format_to(ctx.out(), "vertex: {}, rank: {}", node::vertex(node), node::rank(node));
+ }
+ };
+
+ template
+ class NodeFormatter, Char>
+ {
+ public:
+ constexpr auto parse(std::basic_format_parse_context& ctx) noexcept
+ {
+ return m_Formatter.parse(ctx);
+ }
+
+ template
+ auto format(const decorator::PredecessorNode& node, FormatContext& ctx) const
+ {
+ return std::format_to(
+ m_Formatter.format(node, ctx),
+ ", predecessor: {}",
+ node.predecessor ? std::format("{}", *node.predecessor) : "null");
+ }
+
+ private:
+ SL_UTILITY_NO_UNIQUE_ADDRESS NodeFormatter m_Formatter{};
+ };
+
+ template
+ class NodeFormatter, Char>
+ {
+ public:
+ constexpr auto parse(std::basic_format_parse_context& ctx) noexcept
+ {
+ return m_Formatter.parse(ctx);
+ }
+
+ template
+ auto format(const decorator::DepthNode& node, FormatContext& ctx) const
+ {
+ return std::format_to(
+ m_Formatter.format(node, ctx),
+ ", depth: {}",
+ node.depth);
+ }
+
+ private:
+ SL_UTILITY_NO_UNIQUE_ADDRESS NodeFormatter m_Formatter{};
+ };
+}
+
+template
+struct std::formatter // NOLINT(cert-dcl58-cpp)
+{
+public:
+ constexpr auto parse(std::basic_format_parse_context& ctx)
+ {
+ return m_Formatter.parse(ctx);
+ }
+
+ template
+ auto format(const Node& node, FormatContext& ctx) const
+ {
+ auto out = std::format_to(ctx.out(), "{}", "{");
+ out = m_Formatter.format(node, ctx);
+ return std::format_to(out, "{}", "}");
+ }
+
+private:
+ SL_UTILITY_NO_UNIQUE_ADDRESS sl::graph::NodeFormatter m_Formatter{};
+};
+
+template
+ requires sl::concepts::formattable, Char>
+struct std::formatter // NOLINT(cert-dcl58-cpp)
+{
+ static constexpr auto parse(std::basic_format_parse_context& ctx) noexcept
+ {
+ return ctx.begin();
+ }
+
+ template
+ auto format(const Edge& edge, FormatContext& ctx) const
+ {
+ return std::format_to(ctx.out(), "{}destination: {}{}", "{", sl::graph::edge::destination(edge), "}");
+ }
+};
+
+template
+ requires sl::concepts::formattable, Char>
+ && sl::concepts::formattable, Char>
+struct std::formatter // NOLINT(cert-dcl58-cpp)
+{
+ static constexpr auto parse(std::basic_format_parse_context& ctx) noexcept
+ {
+ return ctx.begin();
+ }
+
+ template