"I got tired of writing the same boilerplate strong type wrappers over and over, so I wrote a tool to generate them. Then I spent 10x more time perfecting the tool than I would've spent just writing the types. This is the way."
A C++ code generator that turns terse descriptions into strongly typed wrappers.
Because execute(int64_t bid_price, int64_t bid_qty, int64_t ask_price, int64_t ask_qty)
is a serious bug and changing it to
execute(Price bid_price, Quantity bid_qty, Price ask_price, Quantity ask_qty)
only buys you just enough time to have a production issue in time to tank next quarter's bonus too.
NOTE This is also an AI playground. I extracted the strong type generation code from a much larger libclang tool, and used it as the kernel for this project. I then used AI for almost everything else. Most of the code in the initial commit, and almost all of the documentation and commits since were AI generated (with some human oversight). I've been playing with different techniques and tools, and this project has been one of the results of that experimentation.
The easiest way to use Atlas in your CMake project:
include(FetchContent)
FetchContent_Declare(Atlas
GIT_REPOSITORY https://github.com/jodyhagins/Atlas.git
GIT_TAG v1.0.0) # Or use 'main' for latest
FetchContent_MakeAvailable(Atlas)
# Generate a strong type with minimal syntax
atlas_add_type(UserId int "<=>" TARGET my_library)That's it! The helper function automatically:
- Generates the header file in your source directory
- Adds it to your target's sources
- Sets up proper build dependencies
# Generate multiple types from a file
add_atlas_strong_types_from_file(
INPUT types.atlas
OUTPUT ${CMAKE_CURRENT_SOURCE_DIR}/Types.hpp
TARGET my_library)
# Generate cross-type interactions (e.g., Price * Quantity -> Total)
add_atlas_interactions_inline(
OUTPUT ${CMAKE_CURRENT_SOURCE_DIR}/Interactions.hpp
TARGET my_library
CONTENT [[
include "Price.hpp"
include "Quantity.hpp"
include "Total.hpp"
namespace=commerce
Price * Quantity -> Total
Price * int <-> Price
]])See docs/AtlasHelpers.md for complete reference of all helper functions, or docs/cmake-integration.md for advanced integration patterns.
Build:
cmake --build --preset debugRun tests:
ctest --preset debugAvailable presets: debug, release, debug-gcc, release-gcc, and ASan variants (debug-clang, release-clang). See cmake --list-presets for the full list.
Install from a release build:
cmake --build --preset release
cmake --install .build/release-clang --prefix /usr/localRun code coverage:
cmake --build build/coverage --target coverage
# See docs/COVERAGE.md for detailsRequirements: C++20 compiler (GCC ≥11, Clang ≥15), CMake ≥3.20, and the crushing realization that strong types actually matter.
The tool requires a C++20 compiler, but generates code that should be usable back to C++11.
Generate a single type:
atlas --kind=struct --namespace=geom --name=Length \
--description="double; +, -, ==, !=" \
--output=Length.hpp
# With named constants
atlas --kind=struct --namespace=geom --name=Distance \
--description="double; +, -, ==, <=>" \
--constants="zero:0.0; max:1000.0" \
--output=Distance.hppGenerate multiple types from a file (batch processing your type safety):
atlas --input=types.atlas --output=strong_types.hpptypes.atlas:
# Optional file-level configuration
guard_prefix=MY_TYPES
# Define reusable profiles
profile=NUMERIC; +, -, *, /, <=>
[type]
kind=struct
namespace=math
name=Distance
description=strong int; {NUMERIC}
default_value=0
constants=zero:0; max:1000
[struct util::Counter]
description=int; ++, --, out
Make types play nicely with each other (like all humans do):
atlas --input=interactions.atlas --interactions=true --output=ops.hppinteractions.atlas:
include "price.hpp"
include "quantity.hpp"
namespace=commerce
Price * Quantity -> Total
Price * int <-> Price # Symmetric (because it's is commutative)
namespace=physics
concept=Numeric
enable_if=std::is_arithmetic_v<Numeric>
Distance * Numeric <-> Distance
Distance / Time -> Velocity
Syntax: [strong] <type>; <options...> (the strong keyword is optional)
Common options (because we're all about that opt-in life):
- Arithmetic:
+,-,*,/,++,--— when your type needs to do math - Comparison:
==,!=,<=>,<,>— for when sorting matters.<=>works in C++11+ via automatic fallback! - Stream:
in,out—std::coutcompatibility sold separately - Callable:
(),(&)— yes, your strong type can be a functor. Why? Because C++. - Pointer-like:
@,->,&of— make it look like a pointer without the segfaults - Containers:
[],iterable— for those fancy data structures - File-level:
auto_hash,auto_ostream,auto_format— enablestd::hash,operator<<,std::formatterfor all types - Constants: Named constants with
constants=name:value; name2:value2— static members like scoped enums - Profiles: Reusable feature bundles with
profile=NAME; features...and{NAME}references - Special:
bool,no-constexpr— the "it's complicated" options
Enforce invariants at construction time and after operations. Invalid states become unrepresentable:
positive— Value must be > 0 (prices, speeds)non_negative— Value must be >= 0 (distances, counts)non_zero— Value must be != 0 (denominators, divisors)bounded<Min,Max>— Value must be in [Min, Max] closed interval (percentages, volume levels)bounded_range<Min,Max>— Value must be in [Min, Max) half-open interval (array indices)non_empty— Container/string must not be empty (usernames, required fields)non_null— Pointer must not be null (required handles, non-null pointers)
Quick example:
[struct audio::Volume]
description=int; bounded<0,100>, +, -, <=>, saturating
[struct finance::Price]
description=double; positive, +, -, *, /
[struct auth::Username]
description=std::string; non_empty, ==
Generated types validate at construction and after operations:
Volume v{150}; // Throws atlas::ConstraintError
Volume a{50}, b{60};
auto c = a + b; // Throws - result 110 exceeds max of 100
Username u{""}; // Throws - empty string not allowed
Username u; // Won't compile - default ctor deleted for non_empty typesConstraints work with all arithmetic modes (checked, saturating, wrapping) and provide compile-time checking for constexpr values.
See the Constrained Types section in the description language docs for complete details.
Full reference (for the masochists): docs/description-language.md
Embed Atlas in your own build tools:
#include "StrongTypeGenerator.hpp"
wjh::atlas::StrongTypeDescription desc{
.kind = "struct",
.type_namespace = "geom",
.type_name = "Length",
.description = "double; +, -",
.constants = {{"zero", "0.0"}, {"max", "1000.0"}}
};
StrongTypeGenerator generator;
std::string header = generator(desc);#include "InteractionGenerator.hpp"
wjh::atlas::InteractionFileDescription desc;
desc.includes = {"\"price.hpp\""};
desc.interactions.push_back({
.op_symbol = "*",
.lhs_type = "Price",
.rhs_type = "Quantity",
.result_type = "Total",
.interaction_namespace = "commerce"
});
std::string header = wjh::atlas::generate_interactions(desc);- Description Language Reference - Complete syntax guide
- Runtime Utilities -
unwrap,undress,castfor accessing wrapped values - CMake Integration - Usage in CMake projects
- Code Coverage - Coverage analysis guide
- Header Guards - Customizing generated guards
Contributions welcome! See docs/CONTRIBUTING.md for guidelines and docs/CODE_OF_CONDUCT.md to learn how to be a decent human while coding.
MIT License. See LICENSE for the legal stuff. TL;DR: Do whatever you want, just don't blame me.