{"id":50312446,"url":"https://github.com/aurimasniekis/cpp-tagval","last_synced_at":"2026-05-28T22:01:35.556Z","repository":{"id":360645894,"uuid":"1245883563","full_name":"aurimasniekis/cpp-tagval","owner":"aurimasniekis","description":"A header-only C++23 library for tagged values","archived":false,"fork":false,"pushed_at":"2026-05-27T08:34:10.000Z","size":101,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-05-27T10:13:08.432Z","etag":null,"topics":["cpp","cpp23","enums","parcel","tagged","tags"],"latest_commit_sha":null,"homepage":"https://aurimasniekis.github.io/cpp-tagval/","language":"C++","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/aurimasniekis.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-05-21T16:47:24.000Z","updated_at":"2026-05-27T08:34:14.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/aurimasniekis/cpp-tagval","commit_stats":null,"previous_names":["aurimasniekis/cpp-tagval"],"tags_count":2,"template":false,"template_full_name":null,"purl":"pkg:github/aurimasniekis/cpp-tagval","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/aurimasniekis%2Fcpp-tagval","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/aurimasniekis%2Fcpp-tagval/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/aurimasniekis%2Fcpp-tagval/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/aurimasniekis%2Fcpp-tagval/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/aurimasniekis","download_url":"https://codeload.github.com/aurimasniekis/cpp-tagval/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/aurimasniekis%2Fcpp-tagval/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":33627941,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-05-28T02:00:06.440Z","response_time":99,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["cpp","cpp23","enums","parcel","tagged","tags"],"created_at":"2026-05-28T22:01:34.610Z","updated_at":"2026-05-28T22:01:35.548Z","avatar_url":"https://github.com/aurimasniekis.png","language":"C++","funding_links":[],"categories":[],"sub_categories":[],"readme":"# tagval\n\n![CI](https://github.com/aurimasniekis/cpp-tagval/actions/workflows/ci.yml/badge.svg)\n[![Docs](https://github.com/aurimasniekis/cpp-tagval/actions/workflows/docs.yml/badge.svg)](https://aurimasniekis.github.io/cpp-tagval/)\n\nA header-only C++23 library for **tagged values** — strongly-typed open and\nclosed enumerations whose entries are first-class compile-time types with a\nstable wire `code`, a human `label`, and optional UI metadata (`icon`,\n`color`). `tagval` is what you reach for when `enum class` runs out of road:\neach kind is a strongly-typed handle, each entry parses from a string,\nformats to a string, hashes, and (optionally) round-trips through JSON or\n[cpp-parcel](https://github.com/aurimasniekis/cpp-parcel).\n\n## Why use this library?\n\n`enum class` gives you compile-time identity and nothing else. `tagval` adds\nthe layers you usually end up reinventing by hand:\n\n- **Stable wire codes.** The `code` survives serialization across versions\n  and never collides with a label.\n- **Human + UI metadata at the entry level.** Optional `label`, `icon`,\n  `color` are attached to each entry as compile-time non-type template\n  parameters — no runtime tables.\n- **Parse from string.** `T::of(\"…\")` throws on miss; `T::try_of(\"…\")`\n  returns `std::expected\u003cT, ParseError\u003e`.\n- **Drop-in formatting.** `std::format(\"{}\", v)`, `std::cout \u003c\u003c v`,\n  `std::hash\u003cT\u003e` and `std::unordered_set\u003cT\u003e` all work out of the box.\n- **Plugin extensibility.** Open-ended kinds accept new entries declared in\n  *other* translation units via `TAGVAL_EXTERN_ENTRY`, without touching the\n  kind class.\n- **Strong typing.** `DeviceKind == Status` doesn't compile; cross-kind\n  handles can never silently coalesce.\n\nNot the right fit if you need: locale-aware label resolution at the library\nlevel, a numeric / bit-flag enumeration, runtime-defined kinds at the type\nlevel, or many thousands of entries per kind (the registry is a linear\nscan; see *Limitations*).\n\n## Quick example\n\n```cpp\n#include \u003ctagval/tagval.hpp\u003e\n\n#include \u003ciostream\u003e\n\nclass Status : public tagval::ClosedEnded\u003c\"status\", Status\u003e {\npublic:\n    using base_t = ClosedEnded;\n    using base_t::base_t;\n\n    TAGVAL_ENTRY(Status, Active, active)\n    TAGVAL_ENTRY(Status, Inactive, inactive, \"Inactive\")\n    TAGVAL_ENTRY_AS(Status, Archived, is_archived, \"archived\", \"Archived\")\n\n    using values_t = tagval::Values\u003cActive, Inactive, Archived\u003e;\n};\n\nint main() {\n    std::cout \u003c\u003c Status::active().code() \u003c\u003c \" — \" \u003c\u003c Status::active().label() \u003c\u003c '\\n';\n    std::cout \u003c\u003c Status::is_archived().code() \u003c\u003c '\\n';  // \"archived\"\n\n    if (auto parsed = Status::try_of(\"inactive\"); parsed) {\n        std::cout \u003c\u003c \"parsed: \" \u003c\u003c *parsed \u003c\u003c '\\n';\n    }\n\n    try {\n        (void)Status::of(\"nope\");\n    } catch (const tagval::UnknownCodeError\u0026 e) {\n        std::cout \u003c\u003c \"rejected: \" \u003c\u003c e.what() \u003c\u003c '\\n';\n    }\n}\n```\n\nWhat's going on:\n\n- `ClosedEnded\u003c\"status\", Status\u003e` is the CRTP base. The string literal\n  `\"status\"` is the kind id and shows up in `kind_id()`, descriptors, and\n  error messages.\n- `TAGVAL_ENTRY(Owner, TypeName, FuncName, ...)` declares a nested `Entry`\n  type and a static accessor of the same name. The wire `code` is the\n  stringified function name (`\"active\"`). Trailing macro arguments fill the\n  optional `Label`, `Icon`, `Color` parameters in that order.\n- `TAGVAL_ENTRY_AS` is the same but takes an explicit code, letting you\n  diverge from the accessor name (`is_archived()` returns the entry whose\n  code is `\"archived\"`).\n- `values_t = tagval::Values\u003c...\u003e` lists every entry. The list is read by\n  `all_values()` and by `value\u003cE\u003e()` to `static_assert` membership.\n- `of()` throws `UnknownCodeError` on a miss; `try_of()` returns\n  `std::expected\u003cStatus, ParseError\u003e` instead.\n\nEvery snippet in this README mirrors a file under `examples/` that CI\ncompiles and runs on every push — if a snippet drifts out of date, the\nbadge above will reflect that.\n\n## Requirements\n\n`tagval` requires a working **C++23** toolchain. The CI matrix in\n`.github/workflows/ci.yml` runs the following on every push:\n\n- Ubuntu (`ubuntu-latest`) with **GCC 14** — Debug and Release.\n- Ubuntu (`ubuntu-latest`) with **Clang 20** — Debug.\n- macOS (`macos-latest`) with the system **Apple Clang** — Debug and\n  Release.\n- ASan + UBSan run, clang-tidy run (on macOS), and a clang-format-22 check.\n\nOther compilers that implement the C++23 features the library uses\n(`concepts`, `std::expected`, inline-variable templates, ranges, NTTP\nclass types) are expected to work but are not gated by CI. MSVC in\nparticular is not exercised — see *Limitations* for the static-archive\ncaveat that affects it.\n\nThe library has one required dependency:\n\n- [`cpp-commons`](https://github.com/aurimasniekis/cpp-commons) ≥ 0.1.3 —\n  provides `comms::FixedString` (the NTTP string type used for kind ids and\n  entry codes) and the `comms::Color` / `comms::Icon` value types used for\n  entry and descriptor metadata. Pulled in automatically by the CMake and\n  Meson builds (target `commons::commons`).\n\nOptional integrations:\n\n- [`nlohmann/json`](https://github.com/nlohmann/json) ≥ 3.12 — JSON\n  adapter.\n- [`cpp-parcel`](https://github.com/aurimasniekis/cpp-parcel) ≥ 0.2 —\n  `TagValCell` envelope. (Also links `commons::commons` transitively.)\n\nBoth adapters are auto-detected via `__has_include`, so simply having the\nheaders visible to the preprocessor is enough.\n\n## Installation\n\n### CMake — FetchContent\n\n```cmake\ncmake_minimum_required(VERSION 3.25)\nproject(my_app LANGUAGES CXX)\n\nset(CMAKE_CXX_STANDARD 23)\nset(CMAKE_CXX_STANDARD_REQUIRED ON)\n\ninclude(FetchContent)\nFetchContent_Declare(tagval\n    URL      https://github.com/aurimasniekis/cpp-tagval/archive/refs/tags/v0.2.0.tar.gz\n    URL_HASH SHA256=c4d982bddf2f65658e287d7a1f932aac2ebbfea93575cad1b80afea297448916\n)\nFetchContent_MakeAvailable(tagval)\n\nadd_executable(my_app main.cpp)\ntarget_link_libraries(my_app PRIVATE tagval::tagval)\n```\n\n### CMake — find_package (after `cmake --install`)\n\n```cmake\nfind_package(tagval 0.2 REQUIRED)\ntarget_link_libraries(my_app PRIVATE tagval::tagval)\n```\n\nInstall rules are skipped automatically if `nlohmann_json`, `parcel`, or\n`commons` came from `FetchContent` — those can't be re-exported. Supply them\nvia `find_package` (and disable `TAGVAL_WITH_NLOHMANN_JSON` /\n`TAGVAL_WITH_PARCEL` if unused) to re-enable installation.\n\n### Meson\n\n```meson\ntagval_dep = dependency('tagval', version: '\u003e=0.2.0',\n    fallback: ['tagval', 'tagval_dep'])\n```\n\n`meson.options` exposes the same toggles (`tests`, `examples`, `json`,\n`parcel`).\n\n### Header-only drop-in\n\nCopy `include/tagval/` onto your include path and add it to your\ncompiler's `-I` flags. The JSON and Parcel adapters auto-activate as soon\nas the relevant third-party headers are on the include path, so no\npreprocessor flags are required when copying.\n\n## Build options\n\nToggles understood by both CMake and Meson (Meson uses lowercase, no\nprefix):\n\n| CMake option                | Default   | What it does                                             |\n|-----------------------------|-----------|----------------------------------------------------------|\n| `TAGVAL_BUILD_TESTS`        | top-level | Build the GoogleTest suite.                              |\n| `TAGVAL_BUILD_EXAMPLES`     | top-level | Build the example binaries.                              |\n| `TAGVAL_BUILD_DOCS`         | OFF       | Build Doxygen HTML docs (`make docs`).                   |\n| `TAGVAL_WITH_NLOHMANN_JSON` | ON        | Link nlohmann/json; enable `\u003ctagval/json_nlohmann.hpp\u003e`. |\n| `TAGVAL_WITH_PARCEL`        | ON        | Link cpp-parcel; enable `\u003ctagval/parcel.hpp\u003e`.           |\n| `TAGVAL_ENABLE_SANITIZERS`  | OFF       | ASan + UBSan in Debug builds.                            |\n| `TAGVAL_ENABLE_CLANG_TIDY`  | OFF       | Run clang-tidy during the build.                         |\n| `TAGVAL_ENABLE_COVERAGE`    | OFF       | Clang source-based coverage.                             |\n| `TAGVAL_WARNINGS_AS_ERRORS` | top-level | Treat compiler warnings as errors.                       |\n| `TAGVAL_INSTALL`            | top-level | Generate install rules.                                  |\n\nThe `Makefile` is a thin wrapper around common workflows: `make test`,\n`make sanitize`, `make tidy`, `make release`, `make coverage`,\n`make docs`, `make no-json`, `make no-parcel`, `make ci` (the full\npre-push gate), `make format`, `make format-check`. Run `make help` for\nthe full list.\n\n## Granular includes\n\n`\u003ctagval/tagval.hpp\u003e` is an umbrella that pulls in every public header.\nIf you'd rather keep a translation unit lean, include only what you use:\n\n| Feature                                                          | Header                            |\n|------------------------------------------------------------------|-----------------------------------|\n| `ClosedEnded` base                                               | `\u003ctagval/closed_ended.hpp\u003e`       |\n| `OpenEnded` base                                                 | `\u003ctagval/open_ended.hpp\u003e`         |\n| `TAGVAL_ENTRY*` macros                                           | `\u003ctagval/macros.hpp\u003e`             |\n| `tagval::Entry` / `TagValMetadata` / `metadata_v`                | `\u003ctagval/entry.hpp\u003e`              |\n| `tagval::Values\u003c…\u003e`                                              | `\u003ctagval/values.hpp\u003e`             |\n| `tagval::OpenEndedRegistry\u003cOwner\u003e` (extern entries for one kind) | `\u003ctagval/openended_registry.hpp\u003e` |\n| `tagval::KindRegistry` (program-wide kind index)                 | `\u003ctagval/kind_registry.hpp\u003e`      |\n| `tagval::TagValDescriptor`                                       | `\u003ctagval/descriptor.hpp\u003e`         |\n| Exception types                                                  | `\u003ctagval/error.hpp\u003e`              |\n| `std::format` integration                                        | `\u003ctagval/format.hpp\u003e`             |\n| `std::ostream` integration                                       | `\u003ctagval/ostream.hpp\u003e`            |\n| `std::hash` specialization                                       | `\u003ctagval/hash.hpp\u003e`               |\n| `nlohmann::json` adapter                                         | `\u003ctagval/json_nlohmann.hpp\u003e`      |\n| `cpp-parcel` adapter                                             | `\u003ctagval/parcel.hpp\u003e`             |\n| Version macros                                                   | `\u003ctagval/version.hpp\u003e`            |\n\n## Core concepts\n\n### Handles\n\nA *handle* is your kind class — `Status`, `DeviceKind`, etc. It derives\nfrom either `tagval::ClosedEnded\u003cId, Self\u003e` or `tagval::OpenEnded\u003cId, Self\u003e`\nvia CRTP. The handle's runtime data is a single `const TagValMetadata*`,\nso handles are trivially copyable and cheap to pass around. A\ndefault-constructed handle is *empty*: `empty() == true`,\n`static_cast\u003cbool\u003e(h) == false`, and `code()` / `label()` return empty\nviews.\n\n### Entries\n\nAn *entry* is a distinct type per value. The `TAGVAL_ENTRY` family of\nmacros declares one:\n\n```cpp\nTAGVAL_ENTRY    (Status, Active,   active)                           // code = \"active\"\nTAGVAL_ENTRY    (Status, Inactive, inactive, \"Inactive\", \"mdi:off\")  // + label, + icon\nTAGVAL_ENTRY_AS (Status, Archived, is_archived, \"archived\", \"Archived\")\n```\n\nEach declaration expands to a nested `struct` deriving from\n`tagval::Entry\u003cOwner, Code, Label, Icon, Color\u003e` plus a static accessor.\nThe accessor returns a `const Status\u0026` referencing a function-local-static\nhandle, so its address is stable.\n\n### `Values\u003c…\u003e`\n\nThe compile-time list of entries:\n\n```cpp\nusing values_t = tagval::Values\u003cActive, Inactive, Archived\u003e;\n```\n\n`Values\u003c…\u003e` static-asserts that every entry's owner is the same type and\nthat no two entries share a `code`. `ClosedEnded::value\u003cE\u003e()` further\nstatic-asserts that `E` is in this list.\n\n### Metadata views\n\n`tagval::TagValMetadata` is the runtime view of an entry — four\n`std::string_view`s (`code`, `label`, `icon`, `color`) that point into the\nentry's NTTP storage, so they're valid for the lifetime of the program. An\nempty `Label` falls back to `code`, so `label()` is never empty for a\nvalid handle. The pinned metadata constant is exposed as\n`tagval::metadata_v\u003cE\u003e`.\n\n### Per-kind extern-entry registry (`OpenEndedRegistry`)\n\n`tagval::OpenEndedRegistry\u003cOwner\u003e` is a per-kind list of metadata\npointers used by `OpenEnded` to merge the compile-time `values_t` entries\nwith any extern entries contributed at static-init time. Predefined\nentries are seeded lazily on first use; extern registrars deduplicate by\ncode. `ClosedEnded` kinds do not use this registry — their values come\nfrom a `constexpr static` array materialized from `values_t`.\n\n### Global kind registry (`KindRegistry`)\n\n`tagval::KindRegistry` is a separate, opt-in, program-wide index of\n*kinds* (not entries). Place `TAGVAL_REGISTER_KIND(MyKind)` at namespace\nscope to add a kind. Once registered, the kind is discoverable through\n`KindRegistry::all()`, `KindRegistry::all_closed()`,\n`KindRegistry::all_open()`, and `KindRegistry::find(kind_id)`. Each\nresult is a `KindView` that exposes the kind's `descriptor()`,\n`category()`, a `values()` snapshot, a zero-allocation `for_each(F)`\nwalk, and a code-based `find()`. Intended for documentation generators\nand other introspection tools.\n\n```cpp\n#include \u003ctagval/tagval.hpp\u003e\n\nclass Status : public tagval::ClosedEnded\u003c\"status\", Status\u003e { /* … */ };\nclass DeviceKind : public tagval::OpenEnded\u003c\"device_kind\", DeviceKind\u003e { /* … */ };\n\nTAGVAL_REGISTER_KIND(Status);\nTAGVAL_REGISTER_KIND(DeviceKind);\n\nvoid emit_docs() {\n    for (const auto\u0026 kv : tagval::KindRegistry::all_closed()) {\n        std::cout \u003c\u003c kv.kind_id() \u003c\u003c \" (closed)\\n\";\n        kv.for_each([](const tagval::KindEntryView\u0026 e) {\n            std::cout \u003c\u003c \"  \" \u003c\u003c e.code \u003c\u003c \" — \" \u003c\u003c e.label \u003c\u003c '\\n';\n        });\n    }\n}\n```\n\n### Descriptors\n\n`tagval::TagValDescriptor` is the runtime view of *kind*-level metadata —\n`id`, `name`, `icon`, `color`. It's always available via\n`Status::descriptor()` (with at least `id` filled in from the kind id);\nopt into the rest by defining `static constexpr make_descriptor()`.\n\n## Closed-ended kinds\n\nA *closed-ended* kind fixes its value set at compile time. Unknown codes\nnever parse, and `value\u003cE\u003e()` ill-formedly refers to entries you forgot to\nlist.\n\n```cpp\n#include \u003ctagval/tagval.hpp\u003e\n\n#include \u003ciostream\u003e\n\nclass Status : public tagval::ClosedEnded\u003c\"status\", Status\u003e {\npublic:\n    using base_t = ClosedEnded;\n    using base_t::base_t;\n\n    static constexpr tagval::TagValDescriptor make_descriptor() noexcept {\n        return tagval::TagValDescriptor{.id = \"status\", .name = \"Status\"};\n    }\n\n    TAGVAL_ENTRY(Status, Active,   active)\n    TAGVAL_ENTRY(Status, Inactive, inactive,    \"Inactive\")\n    TAGVAL_ENTRY_AS(Status, Archived, is_archived, \"archived\", \"Archived\")\n\n    using values_t = tagval::Values\u003cActive, Inactive, Archived\u003e;\n};\n\nint main() {\n    std::cout \u003c\u003c \"kind: \" \u003c\u003c Status::kind_id() \u003c\u003c '\\n';\n    for (const auto\u0026 m : Status::all_values()) {\n        std::cout \u003c\u003c \"  - \" \u003c\u003c m.code \u003c\u003c \" (\" \u003c\u003c m.label \u003c\u003c \")\\n\";\n    }\n\n    std::cout \u003c\u003c \"of('active'): \" \u003c\u003c Status::of(\"active\") \u003c\u003c '\\n';\n\n    try {\n        (void)Status::of(\"nonsense\");\n    } catch (const tagval::UnknownCodeError\u0026 e) {\n        std::cout \u003c\u003c \"rejected: \" \u003c\u003c e.what() \u003c\u003c '\\n';\n    }\n}\n```\n\nNotes worth knowing:\n\n- `Status::value\u003cStatus::Active\u003e() == Status::active()` — both resolve to\n  the same `TagValMetadata` record.\n- `Status::all_values()` returns a `std::span\u003cconst TagValMetadata\u003e`\n  pointing into a `constexpr static` array — valid for the program's\n  lifetime.\n- Calling `Status::value\u003cE\u003e()` with a stray entry whose owner is `Status`\n  but which is missing from `values_t` is a `static_assert` failure, not\n  a runtime miss.\n- The empty `Inactive` icon trick: `TAGVAL_ENTRY(..., \"Inactive\")` skips\n  the icon/color fields. Trailing fields default to empty strings, which\n  parse to \"unset\", so `.icon()` returns an empty `std::optional\u003ccomms::Icon\u003e`\n  and `.color()` an empty `std::optional\u003ccomms::Color\u003e`.\n\n## Open-ended kinds\n\nAn *open-ended* kind has the same in-class declaration shape — `TAGVAL_ENTRY`\nplus `values_t` — and *also* accepts entries from other translation units.\nUseful for plugin systems where the host knows a built-in set and vendors\nextend it without recompiling the kind class.\n\n```cpp\n#include \u003ctagval/tagval.hpp\u003e\n\n#include \u003ciostream\u003e\n\nclass DeviceKind : public tagval::OpenEnded\u003c\"device_kind\", DeviceKind\u003e {\npublic:\n    using base_t = OpenEnded;\n    using base_t::base_t;\n\n    static constexpr tagval::TagValDescriptor make_descriptor() noexcept {\n        return tagval::TagValDescriptor{.id = \"device_kind\", .name = \"Device Kind\"};\n    }\n\n    TAGVAL_ENTRY(DeviceKind, Phone,  phone,  \"Phone\")\n    TAGVAL_ENTRY(DeviceKind, Tablet, tablet, \"Tablet\")\n    TAGVAL_ENTRY(DeviceKind, Laptop, laptop, \"Laptop\")\n\n    using values_t = tagval::Values\u003cPhone, Tablet, Laptop\u003e;\n};\n\nint main() {\n    std::cout \u003c\u003c \"All \" \u003c\u003c DeviceKind::descriptor().name \u003c\u003c \":\\n\";\n    for (const auto\u0026 m : DeviceKind::all_values()) {\n        std::cout \u003c\u003c \"  - \" \u003c\u003c m.code \u003c\u003c \" (\" \u003c\u003c m.label \u003c\u003c \")\\n\";\n    }\n    std::cout \u003c\u003c std::boolalpha\n              \u003c\u003c (DeviceKind::of(\"phone\") == DeviceKind::phone()) \u003c\u003c '\\n';  // true\n}\n```\n\n`all_values()` for an open-ended kind is a `std::ranges::transform_view`\nover the runtime registry. The element type is still\n`const TagValMetadata\u0026`, so range-based for loops and\n`std::ranges::any_of` work unchanged. The first call seeds the registry\nwith the `values_t` entries.\n\n## Plugin / extern entries\n\nTo extend an open-ended kind from another TU (or another library), declare\nentries at namespace scope with `TAGVAL_EXTERN_ENTRY` /\n`TAGVAL_EXTERN_ENTRY_AS`:\n\n```cpp\n#include \u003ctagval/tagval.hpp\u003e\n\n#include \u003ciostream\u003e\n\nclass Plugin : public tagval::OpenEnded\u003c\"plugin\", Plugin\u003e {\npublic:\n    using base_t = OpenEnded;\n    using base_t::base_t;\n\n    TAGVAL_ENTRY(Plugin, Builtin, builtin, \"Built-in\")\n\n    using values_t = tagval::Values\u003cBuiltin\u003e;\n};\n\nnamespace vendor_a {\nTAGVAL_EXTERN_ENTRY(::Plugin, SmartWatch, smart_watch, \"Smart Watch\");\n}\n\nnamespace vendor_b {\nTAGVAL_EXTERN_ENTRY_AS(::Plugin, FridgeCam, fridge_cam, \"fridge_cam\", \"Fridge Cam\");\n}\n\nint main() {\n    for (const auto\u0026 m : Plugin::all_values()) {\n        std::cout \u003c\u003c \"  - \" \u003c\u003c m.code \u003c\u003c \" (\" \u003c\u003c m.label \u003c\u003c \")\\n\";\n    }\n    std::cout \u003c\u003c (Plugin::of(\"smart_watch\") == vendor_a::smart_watch()) \u003c\u003c '\\n';  // 1\n}\n```\n\nWhat the extern macros do:\n\n- Declare a nested entry struct (same as the in-class form).\n- Emit an `inline` accessor function in the surrounding namespace.\n- Emit an `inline` registrar variable whose initializer calls\n  `Registry\u003cOwner\u003e::add(\u0026metadata_v\u003cE\u003e)` at static-init time. The variable\n  is marked `[[gnu::used]]` so GCC and Clang (including Apple Clang) keep\n  it in the binary even when nothing else in the TU is referenced.\n\n`TAGVAL_EXTERN_ENTRY` derives the code from the function name;\n`TAGVAL_EXTERN_ENTRY_AS` takes an explicit code. Registry adds are\nidempotent on `-\u003ecode`, so racing with a redeclaration is harmless.\n\n**Watch out for the static-archive case.** If your vendor TUs are packed\ninto a `.a` and nothing else in those TUs is referenced from the\nconsumer, the linker skips the archive members entirely and the registrar\nnever runs. See *Limitations* below.\n\n## Kind descriptor\n\nDefine `static constexpr make_descriptor()` to attach kind-level\nmetadata. Without it, `descriptor()` still returns `{id = Id}` with the\nother fields empty.\n\n```cpp\n#include \u003ctagval/tagval.hpp\u003e\n\n#include \u003ccommons/literals.hpp\u003e\n\n#include \u003ciostream\u003e\n#include \u003cstring\u003e\n#include \u003cstring_view\u003e\n\nusing namespace comms::literals;\n\nclass Severity : public tagval::ClosedEnded\u003c\"severity\", Severity\u003e {\npublic:\n    using base_t = ClosedEnded;\n    using base_t::base_t;\n\n    static constexpr tagval::TagValDescriptor make_descriptor() noexcept {\n        return tagval::TagValDescriptor{\n            .id    = \"severity\",\n            .name  = \"Alert Severity\",\n            .icon  = \"mdi:alert\"_icon,\n            .color = \"#aa0000\"_color,\n        };\n    }\n\n    TAGVAL_ENTRY(Severity, Info,  info,  \"Info\",  \"mdi:information\",  \"#3366cc\")\n    TAGVAL_ENTRY(Severity, Warn,  warn,  \"Warn\",  \"mdi:alert\",        \"#cc9900\")\n    TAGVAL_ENTRY(Severity, Error, error, \"Error\", \"mdi:alert-circle\", \"#cc0000\")\n\n    using values_t = tagval::Values\u003cInfo, Warn, Error\u003e;\n};\n\nint main() {\n    constexpr auto k = Severity::descriptor();\n    std::cout \u003c\u003c \"Kind: \" \u003c\u003c k.id \u003c\u003c \" — \" \u003c\u003c k.name\n              \u003c\u003c \" (icon=\" \u003c\u003c (k.icon ? k.icon-\u003evalue() : std::string_view{\"-\"}) \u003c\u003c \")\\n\";\n\n    for (const auto\u0026 [code, label, icon, color] : Severity::all_values()) {\n        std::cout \u003c\u003c \"  [\" \u003c\u003c (icon ? icon-\u003evalue() : std::string_view{\"-\"}) \u003c\u003c \"] \" \u003c\u003c code\n                  \u003c\u003c \" (\" \u003c\u003c label \u003c\u003c \") \"\n                  \u003c\u003c (color ? color-\u003eto_hex_string() : std::string{\"-\"}) \u003c\u003c '\\n';\n    }\n}\n```\n\n`make_descriptor()` returns by value at compile time: `id` and `name` are\n`std::string_view`s (so the storage you point at — string literals here —\nmust outlive the descriptor), while `icon` and `color` are\n`std::optional\u003ccomms::Icon\u003e` / `std::optional\u003ccomms::Color\u003e`. The `_icon`\nand `_color` literals validate at compile time; an empty or unparseable\nvalue becomes an empty optional (\"unset\"). Per-entry icon/color are read\nback the same way via `.icon()` / `.color()` on a handle.\n\n## Parsing, formatting, comparing\n\n```cpp\nStatus::of(\"active\");                    // Status — throws UnknownCodeError on miss\nStatus::try_of(\"nope\");                  // std::expected\u003cStatus, ParseError\u003e\nStatus::try_of(\"nope\").error().message();\n// → \"tagval: unknown code 'nope' for kind 'status'\"\n\nstd::format(\"{}\", Status::active());     // \"active\"\nstd::cout \u003c\u003c Status::active();           // active\n\nStatus::active() == Status::of(\"active\");  // true\nStatus::active()  \u003c Status::inactive();    // true — lexicographic on code()\n\nStatus empty;\nstd::format(\"{}\", empty);                // \"\"\nempty \u003c Status::active();                // true — empty sorts before populated\n```\n\n`std::format` accepts only the default spec (`\"{}\"`); anything else\n(`\"{:\u003e10}\"`, `\"{:.5}\"`) throws `std::format_error`. `operator\u003c\u003c` writes\nthe bare `code()`. `operator\u003c=\u003e` returns `std::strong_ordering`, so\nhandles drop straight into `std::set`, `std::map`, and `std::ranges::sort`.\n\n## Hashing \u0026 containers\n\n```cpp\n#include \u003ctagval/tagval.hpp\u003e\n\n#include \u003cunordered_set\u003e\n\nstd::unordered_set\u003cDeviceKind\u003e seen{DeviceKind::phone(), DeviceKind::tablet()};\nseen.contains(DeviceKind::of(\"phone\"));  // true\n```\n\n`std::hash\u003cT\u003e` hashes `(kind_id, code)` so equal handles within a process\nshare a hash and cross-kind handles never collide. Hash values are **not**\nstable across processes — the underlying `std::hash\u003cstd::string_view\u003e` is\nimplementation-defined and may be salted per-run. Use it for in-memory\ncontainers, not for persistent fingerprints.\n\n## JSON support (optional)\n\nActivates when `\u003cnlohmann/json.hpp\u003e` is on the include path (or when\n`TAGVAL_WITH_NLOHMANN_JSON=1` is defined explicitly, which CMake does for\nyou when the option is on).\n\n```cpp\n#include \u003ctagval/tagval.hpp\u003e\n\n#include \u003cnlohmann/json.hpp\u003e\n\n#include \u003ciostream\u003e\n\nclass TransactionType : public tagval::OpenEnded\u003c\"tx_type\", TransactionType\u003e {\npublic:\n    using base_t = OpenEnded;\n    using base_t::base_t;\n\n    TAGVAL_ENTRY(TransactionType, Debit,  debit,  \"Debit\")\n    TAGVAL_ENTRY(TransactionType, Credit, credit, \"Credit\")\n\n    using values_t = tagval::Values\u003cDebit, Credit\u003e;\n};\n\nint main() {\n    const nlohmann::json j = TransactionType::debit();  // \"debit\"\n    const auto recovered = j.get\u003cTransactionType\u003e();\n    std::cout \u003c\u003c recovered \u003c\u003c '\\n';                     // debit\n\n    try {\n        (void)nlohmann::json(\"nonsense\").get\u003cTransactionType\u003e();\n    } catch (const tagval::UnknownCodeError\u0026 e) {\n        std::cout \u003c\u003c \"rejected: \" \u003c\u003c e.what() \u003c\u003c '\\n';\n    }\n}\n```\n\nWire format is the bare code string. `from_json` on an unknown code\nthrows `tagval::UnknownCodeError` — **even for open-ended kinds**.\nDeserialization never auto-creates entries; add them through the macros\ninstead. The same exception type covers `of()` and `j.get\u003cT\u003e()`, so a\nsingle `catch` handles both call paths.\n\n## Parcel support (optional)\n\nActivates when `\u003cparcel/parcel.h\u003e` is on the include path (or when\n`TAGVAL_WITH_PARCEL=1` is defined explicitly).\n\n```cpp\n#include \u003ctagval/tagval.hpp\u003e\n\n#include \u003cparcel/parcel.h\u003e\n\n#include \u003ciostream\u003e\n\nclass PaymentMethod : public tagval::OpenEnded\u003c\"payment_method\", PaymentMethod\u003e {\npublic:\n    using base_t = OpenEnded;\n    using base_t::base_t;\n\n    TAGVAL_ENTRY(PaymentMethod, Card, card, \"Card\")\n    TAGVAL_ENTRY(PaymentMethod, BankTransfer, bank, \"Bank\")\n\n    using values_t = tagval::Values\u003cCard, BankTransfer\u003e;\n};\n\nint main() {\n    using Cell = tagval::TagValCell\u003cPaymentMethod\u003e;\n\n    const Cell cell{PaymentMethod::card()};\n    const auto j = cell.to_json();                     // {\"k\":\"tagval\",\"v\":\"card\"}\n\n    ::parcel::ParcelRegistry reg;\n    reg.register_cells\u003cCell\u003e();\n\n    const auto decoded = reg.cell_from_json(j);\n    if (const auto* typed = dynamic_cast\u003cCell*\u003e(decoded.get()); typed != nullptr) {\n        std::cout \u003c\u003c \"decoded: \" \u003c\u003c typed-\u003evalue \u003c\u003c '\\n';\n    }\n}\n```\n\n**Limitation.** Every `TagValCell\u003cT\u003e` instantiation reports\n`kind_id = \"tagval\"`, so a single `ParcelRegistry` cannot dispatch by\nkind to multiple `TagT`. This matches cpp-parcel's documented constraint\nfor site-knows-the-type usage; if the site really does know the type, the\ninner JSON adapter's `try_of()` still catches cross-kind mismatches with\nnon-overlapping codes.\n\n## Error handling\n\nAll exceptions thrown by `tagval` derive from `tagval::TagValError`, which\nitself derives from `std::runtime_error`. A single `catch (const\ntagval::TagValError\u0026)` handles any error the library raises.\n\n| Mechanism       | When                                                                                                                        | Type                                                |\n|-----------------|-----------------------------------------------------------------------------------------------------------------------------|-----------------------------------------------------|\n| `throw`         | `T::of(code)` miss; `from_json` miss; non-default `std::format` spec (raises `std::format_error`)                           | `tagval::UnknownCodeError` (or `std::format_error`) |\n| `std::expected` | `T::try_of(code)`                                                                                                           | `std::expected\u003cT, tagval::ParseError\u003e`              |\n| `static_assert` | duplicate code in `Values\u003c…\u003e`; empty code in `Entry`; `value\u003cE\u003e()` where `E` is missing from `values_t`; missing `values_t` | compile-time                                        |\n\n```cpp\nauto exp = Status::try_of(\"nope\");\nif (!exp) {\n    const tagval::ParseError\u0026 e = exp.error();\n    std::cout \u003c\u003c e.message() \u003c\u003c '\\n';   // \"tagval: unknown code 'nope' for kind 'status'\"\n    std::cout \u003c\u003c e.code \u003c\u003c '\\n';        // \"nope\"\n    std::cout \u003c\u003c e.kind_id \u003c\u003c '\\n';     // \"status\"\n}\n```\n\n`ParseError::message()` formats the same string `UnknownCodeError::what()`\nexposes, so callers can use either style without rewriting the message.\n\n## Edge cases and pitfalls\n\n- **Empty (default-constructed) handles.** `Status s;` is well-formed.\n  `s.empty()` is true, `static_cast\u003cbool\u003e(s)` is false, and `s.code()` /\n  `s.label()` return empty views. Formatting prints `\"\"`; ordering puts\n  empties before any populated handle. Compare against another empty\n  handle with `==`.\n- **Duplicate codes.** `tagval::Values\u003cActive, Active\u003e` or two entries\n  with the same `Code` string fail to compile via `static_assert` in\n  `Values\u003c…\u003e` — no ambiguous runtime miss.\n- **Empty codes.** `tagval::Entry\u003cOwner, \"\"\u003e` fails to compile via\n  `static_assert` in `Entry`.\n- **Cross-kind comparison.** `KindA::x() == KindB::x()` is ill-formed by\n  design; cross-kind values can never silently coalesce. If you really\n  need to compare across kinds, compare `kind_id()` and `code()`\n  explicitly.\n- **Hash is per-process.** Don't persist `std::hash\u003cT\u003e(v)` to disk or\n  send it over the wire — use `code()` (and `kind_id()`) instead.\n- **Static-init ordering across translation units.** Extern entries\n  register themselves at static-init time. Looking one up from another\n  static initializer that runs *before* the registrar is undefined; defer\n  such lookups to function bodies (called after `main()` begins, or via\n  Meyers singletons). Within a single TU, ordering is well-defined.\n- **Static archives drop the registrar.** Both\n  `TAGVAL_EXTERN_ENTRY` and `TAGVAL_REGISTER_KIND` emit an inline\n  `[[gnu::used]]` variable whose initializer runs at static-init. The\n  attribute keeps the variable from being dead-stripped *after* the\n  object is linked in; it does **not** override the archive selector. If\n  the TU containing the registrar is only inside a `.a` and nothing else\n  in that TU is referenced from the consumer, the linker skips the\n  entire archive member and the registrar never runs — the entry or\n  kind silently fails to appear. Workarounds: link the registrar\n  objects directly (CMake `OBJECT` library / Meson source list),\n  reference one symbol per registrar TU from the consumer, or use\n  `-Wl,--whole-archive` (GNU) / `-Wl,-force_load` (Apple). MSVC\n  additionally needs `/INCLUDE:\u003cmangled-name\u003e` to keep the inline\n  registrar; CI does not exercise MSVC.\n- **`from_json` never auto-creates entries.** Open-ended kinds still\n  reject unknown codes on deserialization. Add entries through\n  `TAGVAL_EXTERN_ENTRY`.\n- **`std::format` is strict.** Only `\"{}\"` is accepted; `\"{:\u003e10}\"`,\n  `\"{:.3}\"`, etc. throw `std::format_error`.\n- **Registry is not thread-safe.** `Registry\u003cOwner\u003e::add()` mutates a\n  per-kind vector and is only safe during static-init (single-threaded by\n  contract). After `main()` begins the registry is read-only and\n  concurrent reads from any thread are fine.\n- **TagValDescriptor string lifetime.** The descriptor stores\n  `std::string_view`s. If `make_descriptor()` returns views into\n  function-local storage, those views dangle. Use string literals.\n\n## API overview\n\nThe umbrella `\u003ctagval/tagval.hpp\u003e` exposes the following public surface\nunder `namespace tagval`:\n\n| Symbol                                      | Purpose                                          | Notes                                                               |\n|---------------------------------------------|--------------------------------------------------|---------------------------------------------------------------------|\n| `ClosedEnded\u003cId, Self\u003e`                     | CRTP base for fixed-set kinds                    | `value\u003cE\u003e()`, `of`, `try_of`, `all_values`, `kind_id`, `descriptor` |\n| `OpenEnded\u003cId, Self\u003e`                       | CRTP base for plugin-extensible kinds            | Same surface; registry-backed                                       |\n| `Entry\u003cOwner, Code, Label, Icon, Color\u003e`    | Compile-time entry record                        | Subclass via `TAGVAL_ENTRY*`                                        |\n| `Values\u003cE…\u003e`                                | Compile-time list of entries                     | Static-asserts owner + code uniqueness                              |\n| `OpenEndedRegistry\u003cOwner\u003e`                  | Per-kind runtime registry of extern entries      | Mutate only at static-init                                          |\n| `KindRegistry`                              | Program-wide index of registered kinds           | Opt-in via `TAGVAL_REGISTER_KIND`                                   |\n| `KindView`                                  | Type-erased handle to one registered kind        | `descriptor()`, `category()`, `values()`, `for_each()`, `find()`    |\n| `KindCategory`                              | Closed / Open enum                               | Returned by `KindView::category()`                                  |\n| `TagValMetadata`                            | Runtime view of an entry (code/label/icon/color) | Pointers stable for program lifetime                                |\n| `TagValDescriptor`                          | Runtime view of kind-level metadata              | Provided by `descriptor()`                                          |\n| `TagValError`, `UnknownCodeError`           | Exception types                                  | Derive from `std::runtime_error`                                    |\n| `ParseError`                                | `try_of` failure record                          | Has `code`, `kind_id`, `message()`                                  |\n| `comms::FixedString\u003cN\u003e`                     | NTTP-friendly string class (from cpp-commons)    | Used for kind id and entry code                                     |\n| `metadata_v\u003cE\u003e`                             | Pinned `TagValMetadata` constant for entry `E`   | ODR-merged across TUs                                               |\n| `TagValCell\u003cTagT\u003e` (optional)               | cpp-parcel envelope                              | `kind_id = \"tagval\"`                                                |\n| `TAGVAL_ENTRY[_AS]`                         | In-class entry macro                             | Derived or explicit code                                            |\n| `TAGVAL_EXTERN_ENTRY[_AS]`                  | Extern entry macro                               | Registers an entry into `OpenEndedRegistry\u003cOwner\u003e` at static-init   |\n| `TAGVAL_REGISTER_KIND(K)`                   | Kind-registration macro                          | Adds a kind to `KindRegistry` at static-init                        |\n| `TAGVAL_VERSION_{MAJOR,MINOR,PATCH,STRING}` | Header version macros                            | `\u003ctagval/version.hpp\u003e`                                              |\n\nInternal helpers under `tagval::detail` (e.g. `HandleBase`,\n`TagValBaseTag`) are not part of the public API and may change between\npatch releases.\n\n## Examples\n\nThe `examples/` directory is built by `make examples` and run on every\nCI push:\n\n| Example                           | Demonstrates                                                     |\n|-----------------------------------|------------------------------------------------------------------|\n| `examples/closed_ended.cpp`       | A `ClosedEnded` kind end-to-end: declarations, `of`, descriptor. |\n| `examples/open_ended.cpp`         | An `OpenEnded` kind with predefined entries only.                |\n| `examples/extern_entries.cpp`     | Plugin entries via `TAGVAL_EXTERN_ENTRY[_AS]`.                   |\n| `examples/metadata.cpp`           | Kind-level `make_descriptor()` plus per-entry icon/color.        |\n| `examples/formatting.cpp`         | `std::format` and `operator\u003c\u003c`.                                  |\n| `examples/json_integration.cpp`   | nlohmann/json round-trip + error path.                           |\n| `examples/parcel_integration.cpp` | cpp-parcel cell round-trip.                                      |\n\n## Testing\n\n```bash\ncmake -S . -B build\ncmake --build build\nctest --test-dir build --output-on-failure\n```\n\nOr simply `make test`. The suite includes `tagval_test_extern_split`,\nwhich compiles two vendor TUs into a CMake `OBJECT` library and verifies\nthat `[[gnu::used]]` keeps the registrar symbols alive — a regression\ntest for the static-archive caveat above.\n\nRun the full pre-push gate (the same checks CI runs) with `make ci`. The\nclang-format step requires **clang-format-22** specifically; older\nversions may produce a different diff.\n\n## Limitations\n\n- **Linear-scan lookup.** `Registry\u003cOwner\u003e` and `ClosedEnded::all_values()`\n  use linear scans for `of()` / `try_of()`. For typical kinds (a handful\n  to a few dozen entries) this is faster than hashing. Add a hash\n  side-index if you genuinely have many hundreds of values.\n- **Cross-kind comparison is rejected by design.** `DeviceKind ==\n  Status` doesn't compile; compare `kind_id()` and `code()` explicitly\n  if you need the looser semantics.\n- **`TagValCell\u003cT\u003e` shares `kind_id=\"tagval\"`** across all\n  instantiations, so a single `ParcelRegistry` can't dispatch by kind to\n  multiple `TagT`. Site-knows-the-type usage is fine; multi-kind\n  dispatch isn't.\n- **Static-init ordering caveat** for extern entries: lookups from\n  another static initializer that runs before the registrar are\n  undefined. Defer to function bodies.\n- **Static-archive linker culling.** GCC/Clang `[[gnu::used]]` keeps the\n  registrar alive *within* a linked-in object; it doesn't force the\n  archive selector to pull the object in. MSVC needs `/INCLUDE:` and is\n  not exercised by CI.\n\n## FAQ\n\n**Is the library header-only?** Yes — including `\u003ctagval/tagval.hpp\u003e`\n(or any individual header) is all you need at compile time. There's no\n`tagval.cpp` and no precompiled binary. The CMake `tagval` target is an\n`INTERFACE` library that only carries include paths and the optional\ndependency links.\n\n**What happens if a code is invalid?** `T::of(\"...\")` throws\n`tagval::UnknownCodeError`; `T::try_of(\"...\")` returns\n`std::expected\u003cT, ParseError\u003e` with the error filled in. Duplicate or\nempty codes are rejected at compile time via `static_assert`.\n\n**Can I use it in multiple threads?** Reads (`code`, `label`, `icon`,\n`color`, `of`, `try_of`, `all_values`) are safe to call concurrently.\nThe registry's mutating path (extern registration) is single-threaded\nby contract — it only runs at static-init.\n\n**Does the handle own its strings?** No. Handles hold a pointer to a\n`TagValMetadata` whose `string_view`s point into the entry's NTTP\nstorage. That storage lives for the program's lifetime, so handles are\ntrivially copyable and can be passed by value freely.\n\n**Which compilers are supported?** GCC 14, Clang 20, and Apple Clang\n(macOS-latest) are gated by CI on every push. Other C++23-conformant\ncompilers should work; MSVC requires manual `/INCLUDE:` linker\narguments for extern entries in static archives and is not exercised\nby CI.\n\n**My extern entry isn't showing up in `all_values()`. What's wrong?**\nAlmost always the static-archive case described under *Limitations*:\nthe linker culled the entire `.o` containing the registrar because no\nother symbol in it was referenced from the consumer. Switch the vendor\nsources to a CMake `OBJECT` library (or Meson source list) so the\nobjects are linked directly, or pass `-Wl,--whole-archive` / `-Wl,-force_load`.\n\n**How do I debug build errors?** The most common compile-time errors\nhave static-assert messages with `tagval:` in them — search the build\nlog for that prefix. The library uses concept-constrained\nspecializations for `std::hash`, `std::formatter`, and `operator\u003c\u003c`, so\nunrelated overload sets aren't polluted.\n\n## Contributing\n\nContributions to the library are welcome! If you encounter any issues or have suggestions for\nimprovements,\nplease feel free to submit a pull request or open an issue on the project's repository.\n\n## License\n\nThis project is licensed under the MIT License. See the [LICENSE](LICENSE) file for details.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Faurimasniekis%2Fcpp-tagval","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Faurimasniekis%2Fcpp-tagval","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Faurimasniekis%2Fcpp-tagval/lists"}