@codejen/pack-cpp

recipes.

Every generator, with the exact command and the output it prints.

Modern C++ scaffolding and app starters.

Install

Three ways to get the pack, depending on how much you want on your machine. The generators are the same in every case.

Globally

One install, every project. jen finds globally installed packs next to itself, without listing them anywhere, so this also works in projects without a package.json, like most C++ repos.

$ npm install -g @codejen/jen @codejen/pack-cpp
$ jen cpp:sdl3 --name=MyGame

Locally best for monorepos

Add both as dev dependencies. jen reads packs straight from the project's package.json, so the generator versions are pinned with the repo: every contributor and every CI run gets the same ones, and there's nothing to set up on anyone's machine. In a monorepo, put them in the root.

$ npm install --save-dev @codejen/jen @codejen/pack-cpp
$ npx jen cpp:sdl3 --name=MyGame

Not at all

Nothing to install but jen itself, or not even that with npx. When you ask for a prefixed generator like cpp:sdl3 and the pack isn't installed, jen fetches @codejen/pack-cpp via npm into a throwaway directory, runs it once and removes it again: no package.json entry, no node_modules left behind.

$ npx @codejen/jen cpp:sdl3 --name=MyGame

That automatic fetch only ever pulls from the @codejen scope, so a mistyped name can't install someone else's package; set JEN_NO_FETCH=1 to turn it off. For a pack from any other scope, or a Yeoman generator, be explicit with --from.

$ npx @codejen/jen --from @codejen/pack-cpp cpp:sdl3 --name=MyGame

Where files go

Generators write into the folder you run them in, or --dir: from the project root, cpp:class --name=Foo --dir=src writes src/Foo.h. The app starters make a new project folder in the current directory instead; --dir names it. The home page has the general rules.

From src/net/, cpp:class --name=Socket writes src/net/Socket.h and wires net/Socket.cpp into the CMakeLists.txt that carries # jen:sources, which jen finds by walking up from where you are. Wiring is looked up by marker, never guessed:

  • --dir: write somewhere else, relative to the current directory.
  • --withTest: the test goes next to the header and is wired into the CMakeLists.txt that carries # jen:tests.
  • No marker: the plan shows the insert as skipped; add the marker and run the generator again.

The tooling generators (cpp:warnings, cpp:tidy, cpp:doctest, …) work on the project as a whole, so their files go to the project root from wherever you are.

App starters

Every starter creates a new project folder named after the app (MyGame becomes my-game/), so cd into it before running the other generators. All of them split the project into a <name>-core library that holds every source file and a thin <name> executable that holds only main.cpp, which is what makes the code linkable from tests.

cpp:sdl3

An SDL3 callbacks-based app: root CMakeLists.txt, a vendor/ that FetchContents SDL3 (a local find_package is tried first, so nothing touches the network if SDL3 is already installed), and an app class in the core library clearing the window to an animated color.

$ jen cpp:sdl3 --name=MyGame
cpp:sdl3 (pack @codejen/pack-cpp) – create an SDL3 callback-based app starter (<name>-core library + thin executable, vendor/ FetchContent SDL3)

  + my-game/CMakeLists.txt
  + my-game/vendor/CMakeLists.txt
  + my-game/src/CMakeLists.txt
  + my-game/src/main.cpp
  + my-game/src/app.h
  + my-game/src/app.cpp

✓ 6 written

Params

name

project and target name, window title and app id

required

width

window width in pixels

default 800

height

window height in pixels

default 600

bundleId

app identifier, e.g. com.example.my-game (default: com.example.<name>)

folderCase

project folder name from --name: kebab (my-game) or pascal (MyGame)

default kebab

one of kebab, pascal

dir

project folder instead of one named after --name; . for the current directory

sdlTag

SDL3 git tag to fetch

default release-3.4.14

cpp:sdl3-opengl

The same starter with an OpenGL 4.1 core context instead: vendors a real, checked-in glad loader under vendor/glad/, adds cmake/embed-glsl.cmake, and scaffolds an app class that compiles a shader pair and draws an animated full-screen quad.

$ jen cpp:sdl3-opengl --name=MyGame
cpp:sdl3-opengl (pack @codejen/pack-cpp) – create an SDL3 + OpenGL (4.1 core, glad) app starter with a shader-quad demo; vendors glad and wires up cmake/embed-glsl.cmake

  + my-game/CMakeLists.txt
  + my-game/vendor/CMakeLists.txt
  + my-game/cmake/embed-glsl.cmake
  + my-game/vendor/glad/CMakeLists.txt
  + my-game/vendor/glad/include/glad/glad.h
  + my-game/vendor/glad/include/KHR/khrplatform.h
  + my-game/vendor/glad/src/glad.c
  + my-game/src/CMakeLists.txt
  + my-game/src/main.cpp
  + my-game/src/app.h
  + my-game/src/app.cpp
  + my-game/src/shader-utils.h
  + my-game/src/shader-utils.cpp
  + my-game/src/quad.vert.glsl
  + my-game/src/quad.frag.glsl

✓ 15 written

Params

name

project and target name, window title and app id

required

width

window width in pixels

default 800

height

window height in pixels

default 600

bundleId

app identifier, e.g. com.example.my-game (default: com.example.<name>)

folderCase

project folder name from --name: kebab (my-game) or pascal (MyGame)

default kebab

one of kebab, pascal

dir

project folder instead of one named after --name; . for the current directory

sdlTag

SDL3 git tag to fetch

default release-3.4.14

cpp:app

A console starter without SDL. main(int argc, char** argv) wraps the arguments in a std::span to avoid pointer arithmetic (bounds profile).

$ jen cpp:app --name=MyTool
cpp:app (pack @codejen/pack-cpp) – create a console app starter without SDL (<name>-core library + thin executable, main wraps argv in a span)

  + my-tool/CMakeLists.txt
  + my-tool/src/CMakeLists.txt
  + my-tool/src/main.cpp
  + my-tool/src/app.h
  + my-tool/src/app.cpp

✓ 5 written

Params

name

project and target name

required

folderCase

project folder name from --name: kebab (my-game) or pascal (MyGame)

default kebab

one of kebab, pascal

dir

project folder instead of one named after --name; . for the current directory

cpp:lib

A library starter: public headers in include/<name>/, BUILD_INTERFACE/INSTALL_INTERFACE include dirs, a generated export header, and install rules plus a CMake package config, so consumers can find_package(my-lib) and link my-lib::my-lib. The target is called <name>-core internally like in the other starters (so tests and cpp:embed find it) and is exported as <name>::<name>.

$ jen cpp:lib --name=MyLib
cpp:lib (pack @codejen/pack-cpp) – create a library starter: include/<name>/, BUILD_INTERFACE/INSTALL_INTERFACE, export header, install + package config

  + my-lib/CMakeLists.txt
  + my-lib/src/CMakeLists.txt
  + my-lib/include/my-lib/my-lib.h
  + my-lib/src/my-lib.cpp
  + my-lib/cmake/my-libConfig.cmake.in

✓ 5 written

Params

name

project and target name; headers go in include/<name>/

required

folderCase

project folder name from --name: kebab (my-game) or pascal (MyGame)

default kebab

one of kebab, pascal

dir

project folder instead of one named after --name; . for the current directory

cpp:module

Experimental. A C++20 modules starter: a .cppm module interface in a FILE_SET CXX_MODULES of the core library, imported by main.cpp. Needs CMake 3.28 or newer and a module-capable generator (cmake -G Ninja).

$ jen cpp:module --name=MyMod
cpp:module (pack @codejen/pack-cpp) – (experimental) create a C++20 modules starter: .cppm in a FILE_SET CXX_MODULES, CMake >= 3.28, needs Ninja

  + my-mod/CMakeLists.txt
  + my-mod/src/CMakeLists.txt
  + my-mod/src/my_mod.cppm
  + my-mod/src/main.cpp

✓ 4 written

Params

name

project and target name

required

folderCase

project folder name from --name: kebab (my-game) or pascal (MyGame)

default kebab

one of kebab, pascal

dir

project folder instead of one named after --name; . for the current directory

Core library and markers

The starters leave marker comments in the files they write, like # jen:sources in src/CMakeLists.txt. When you run another generator afterwards, say cpp:class, it inserts its new line at the matching marker instead of guessing where it belongs. jen looks for the nearest file with the marker, walking up from where the files go, so a subfolder can have its own CMakeLists.txt. If a project has no such marker (it wasn't made from a starter), the plan shows that insert as skipped: add the marker and run the generator again. Running it twice is safe: the second time it just reports "already present".

Each card below is one marker: the file it lives in, and the generators that insert there.

Markers

# jen:sources

cpp:class, cpp:interface --impl, cpp:pimpl

in src/CMakeLists.txt

# jen:embed

cpp:embed, cpp:shader

in src/CMakeLists.txt

# jen:link

cpp:warnings, cpp:compiler, cpp:tracy

in src/CMakeLists.txt

# jen:app

cpp:icon (settings for the executable only)

in src/CMakeLists.txt

# jen:options

cpp:tidy, cpp:warnings, cpp:compiler, cpp:tracy, cpp:cpack --staticSdl

in CMakeLists.txt

# jen:subdirs

cpp:doctest

in CMakeLists.txt

# jen:install, # jen:cpack

cpp:cpack (CPack settings come last, after every install())

in CMakeLists.txt

# jen:tests

--withTest (set up by cpp:doctest)

in tests/CMakeLists.txt

// jen:includes

cpp:tracy

in src/app.cpp

// jen:frame-end

cpp:tracy

in src/app.cpp, end of SDL_AppIterate

// jen:gl-init

cpp:tracy --gpu

in src/app.cpp, after the glad load (OpenGL starter)

The tooling generators write a cmake/<name>.cmake and add an include(...) at # jen:options, and refer to the core library as ${PROJECT_NAME}-core.

Classes and types

cpp:class

Plain header/source class, wired into its CMakeLists.txt's sources list at the # jen:sources marker.

$ jen cpp:class --name=rigid_body --dir=src
cpp:class (pack @codejen/pack-cpp) – create a C++ class (header + source), wired into the CMakeLists.txt with # jen:sources

  + src/RigidBody.h
  + src/RigidBody.cpp
  ~ src/CMakeLists.txt  + RigidBody.cpp

✓ 3 written

cpp:class --moveOnly

A move-only RAII wrapper instead (Rule of Five): deleted copy, noexcept move via release()/reset(), [[nodiscard]] get()/release(), and an explicit operator bool(). Use it for C-APIs with a non-trivial destroy call (raii in my-cpp-snippets).

$ jen cpp:class --name=Texture --moveOnly --resource=GLuint --nullValue=0 --destroy=glDeleteTextures --dir=src
cpp:class (pack @codejen/pack-cpp) – create a C++ class (header + source), wired into the CMakeLists.txt with # jen:sources

  + src/Texture.h
  + src/Texture.cpp
  ~ src/CMakeLists.txt  + Texture.cpp

✓ 3 written

Params

name

the class, any case: player-controller and PlayerController both make PlayerController.h and PlayerController.cpp

required

namespace

C++ namespace to wrap it in, nested ones as engine::physics; empty means the global namespace

moveOnly

RAII wrapper: copy deleted, move defaulted (Rule of Five)

default false

resource

with --moveOnly: the handle type it owns, e.g. SDL_Window* or GLuint

default Resource*

nullValue

with --moveOnly: the empty handle value, e.g. 0 for GLuint

default nullptr

destroy

with --moveOnly: the function that frees the handle, e.g. SDL_DestroyWindow

default destroy

withTest

add a doctest case (needs cpp:doctest)

default false

Every generator shows its params in the terminal too, with --help:

$ jen cpp:class --help
cpp:class – create a C++ class (header + source), wired into the CMakeLists.txt with # jen:sources
  --name (required)  the class, any case: player-controller and PlayerController both make PlayerController.h and PlayerController.cpp
  --namespace=""  C++ namespace to wrap it in, nested ones as engine::physics; empty means the global namespace
  --moveOnly  RAII wrapper: copy deleted, move defaulted (Rule of Five)
  --resource=Resource*  with --moveOnly: the handle type it owns, e.g. SDL_Window* or GLuint
  --nullValue=nullptr  with --moveOnly: the empty handle value, e.g. 0 for GLuint
  --destroy=destroy  with --moveOnly: the function that frees the handle, e.g. SDL_DestroyWindow
  --withTest  add a doctest case (needs cpp:doctest)

cpp:handle

A generic, header-only Handle<T, Deleter, Null> template for non-pointer handles (GLuint, a file descriptor, …), constrained with std::regular/std::invocable (raiit in my-cpp-snippets).

$ jen cpp:handle --name=Handle --dir=src
cpp:handle (pack @codejen/pack-cpp) – create a generic Handle<T, Deleter, Null> RAII template for non-pointer handles (header-only)

  + src/Handle.h

✓ 1 written

Params

name

the template, any case: gl-handle and GlHandle both make GlHandle in GlHandle.h

required

namespace

C++ namespace to wrap it in, nested ones as engine::physics; empty means the global namespace

cpp:scopeexit

A header-only ScopeExit scope-guard template: runs a noexcept-checked callable on destruction unless release()d first (scopeexit in my-cpp-snippets).

$ jen cpp:scopeexit --name=ScopeExit --dir=src
cpp:scopeexit (pack @codejen/pack-cpp) – create a ScopeExit scope-guard template (nothrow-enforced, header-only)

  + src/ScopeExit.h

✓ 1 written

Params

name

the template, any case: scope-exit and ScopeExit both make ScopeExit in ScopeExit.h

required

namespace

C++ namespace to wrap it in, nested ones as engine::physics; empty means the global namespace

cpp:r0

A Rule of Zero class: no special member functions, one constructor generated from --members, private members set from the moved-from constructor params (r0 in my-cpp-snippets).

$ jen cpp:r0 --name=Person --members="std::string name, int age" --dir=src
cpp:r0 (pack @codejen/pack-cpp) – create a Rule of Zero class: members generated from constructor params, no special member functions (header-only)

  + src/Person.h

✓ 1 written

Params

name

the class, any case: movie-info and MovieInfo both make MovieInfo in MovieInfo.h

required

members

comma-separated "Type name" pairs, e.g. "std::string title, int year"

required

namespace

C++ namespace to wrap it in, nested ones as engine::physics; empty means the global namespace

compare

add a defaulted <=> (comparison and ==)

default false

withTest

add a doctest case (needs cpp:doctest)

default false

cpp:interface

A polymorphic base class (C.35, C.67, C.128): virtual defaulted destructor, protected copy/move to prevent slicing, pure virtual methods from --methods. With --impl it also scaffolds a final derived class with all the overrides, stub bodies in a .cpp wired in at # jen:sources. Not sure it's the right tool? See virtual, variant or templates?.

$ jen cpp:interface --name=Drawable --methods="void draw(), void resize(int w, int h)" --impl=SpriteDrawable --dir=src
cpp:interface (pack @codejen/pack-cpp) – create a polymorphic interface class (virtual dtor, protected copy/move, pure virtual methods)

  + src/Drawable.h
  + src/SpriteDrawable.h
  + src/SpriteDrawable.cpp
  ~ src/CMakeLists.txt  + SpriteDrawable.cpp

✓ 4 written

Params

name

the interface, any case: shape and Shape both make Shape in Shape.h

required

methods

comma-separated pure virtual methods, e.g. "void draw() const, bool resize(int w, int h)"

impl

also create a final class with this name that implements the interface

namespace

C++ namespace to wrap it in, nested ones as engine::physics; empty means the global namespace

withTest

add a doctest case (needs cpp:doctest)

default false

cpp:struct

An aggregate for plain data bundles without an invariant (C.2, C.131): public members with default member initializers (C.48), so designated initializers work (Config{.width = 800}). Members without an initializer get {}.

$ jen cpp:struct --name=Config --members="int width = 800, int height = 600" --compare --dir=src
cpp:struct (pack @codejen/pack-cpp) – create an aggregate struct with default member initializers for plain data bundles (header-only)

  + src/Config.h

✓ 1 written

Params

name

the struct, any case: window-config and WindowConfig both make WindowConfig in WindowConfig.h

required

members

comma-separated "Type name [= value]" entries, e.g. "int width = 800, int height = 600"

required

compare

add a defaulted <=> (comparison and ==)

default false

namespace

C++ namespace to wrap it in, nested ones as engine::physics; empty means the global namespace

withTest

add a doctest case (needs cpp:doctest)

default false

cpp:strong

A strong typedef (I.4): explicit constructor, value(), and optional operators. Keeps pixels, texels and world units apart.

$ jen cpp:strong --name=Pixels --underlying=int --ops=compare,arith --dir=src
cpp:strong (pack @codejen/pack-cpp) – create a strong typedef: explicit constructor, optional <=> / arithmetic / std::hash (header-only)

  + src/Pixels.h

✓ 1 written

Params

name

the type, any case: pixels and Pixels both make Pixels in Pixels.h

required

underlying

the wrapped type, e.g. float or int

required

ops

comma-separated: compare (<=>), arith (+ - * /), hash (std::hash)

default compare

namespace

C++ namespace to wrap it in, nested ones as engine::physics; empty means the global namespace

withTest

add a doctest case (needs cpp:doctest)

default false

cpp:enum

An enum class with to_string() and a std::formatter specialization (Enum.3), so std::print("{}", GameState::Playing) just works. The switch has no default on purpose: -Wswitch then warns when a value is added but to_string isn't updated. Header-only.

$ jen cpp:enum --name=GameState --values="menu, playing, paused" --dir=src
cpp:enum (pack @codejen/pack-cpp) – create an enum class with to_string() and a std::formatter specialization (header-only)

  + src/GameState.h

✓ 1 written

Params

name

the enum, any case: game-state and GameState both make GameState in GameState.h

required

values

comma-separated enumerators, e.g. "idle, running, paused"

required

namespace

C++ namespace to wrap it in, nested ones as engine::physics; empty means the global namespace

std

C++ standard: 23, or 20 (std::abort instead of std::unreachable)

default 23

one of 20, 23

withTest

add a doctest case (needs cpp:doctest)

default false

cpp:pimpl

A pimpl class (I.27): std::unique_ptr<Impl>, with the destructor and move operations defaulted in the .cpp so Impl can stay incomplete in the header. Easy to get wrong by hand.

$ jen cpp:pimpl --name=Renderer --dir=src
cpp:pimpl (pack @codejen/pack-cpp) – create a pimpl class: unique_ptr<Impl>, special members defaulted in the .cpp; wired into the CMakeLists.txt with # jen:sources

  + src/Renderer.h
  + src/Renderer.cpp
  ~ src/CMakeLists.txt  + Renderer.cpp

✓ 3 written

Params

name

the class, any case: renderer and Renderer both make Renderer.h and Renderer.cpp

required

namespace

C++ namespace to wrap it in, nested ones as engine::physics; empty means the global namespace

withTest

add a doctest case (needs cpp:doctest)

default false

cpp:variant

A std::variant of small case structs plus the overloaded visitor helper (in its own src/overloaded.h, shared between variants): a modern alternative to a class hierarchy, e.g. for events or render commands. Often the better pick when the set of cases is closed, see virtual, variant or templates?.

$ jen cpp:variant --name=Event --cases="KeyDown, KeyUp, Resize" --dir=src
cpp:variant (pack @codejen/pack-cpp) – create a std::variant of case structs plus the overloaded visitor helper (header-only)

  + src/overloaded.h
  + src/Event.h

✓ 2 written

Params

name

the variant, any case: input-event and InputEvent both make InputEvent in InputEvent.h, plus a shared overloaded.h

required

cases

comma-separated case structs, e.g. "click, keyPress, resize"

required

namespace

C++ namespace to wrap it in, nested ones as engine::physics; empty means the global namespace

withTest

add a doctest case (needs cpp:doctest)

default false

--withTest

cpp:class, cpp:r0, cpp:struct, cpp:strong, cpp:enum, cpp:pimpl, cpp:variant and cpp:interface take --withTest. It scaffolds <Name>_test.cpp next to the header with a doctest case checking what the generator promises (move-only, aggregate, abstract, …), and adds it to the CMakeLists.txt that carries # jen:tests. The checks describe the class as generated, so update them when you change the design: give a class a constructor with arguments and the default-constructible check has to go. Run cpp:doctest first, otherwise only the test file is written and the insert is reported as skipped.

$ jen cpp:class --name=Camera --withTest --dir=src
cpp:class (pack @codejen/pack-cpp) – create a C++ class (header + source), wired into the CMakeLists.txt with # jen:sources

  + src/Camera.h
  + src/Camera.cpp
  ~ src/CMakeLists.txt  + Camera.cpp
  + src/Camera_test.cpp
  ~ tests/CMakeLists.txt  + ../src/Camera_test.cpp

✓ 5 written

Virtual, variant or templates?

Polymorphism is still good practice, but it isn't the default. Pick by asking who can add new types:

open: plugins, drivers, code you haven't written yet

cpp:interface

use virtual functions

closed: events, render commands, AST nodes

cpp:variant

use std::variant + std::visit

known at compile time

cpp:handle, cpp:scopeexit show the style

use templates and concepts

just one operation

—

use std::function / std::move_only_function

What does virtual cost in a game loop? A single virtual call is cheap: one indirect jump, well predicted when the same type shows up repeatedly. The expensive parts are around it:

  • the call can't be inlined, so the compiler can't optimize across it or vectorize the loop;
  • objects live behind pointers, usually scattered on the heap, so iterating them is a cache miss per element;
  • with mixed types in one container, the indirect branch is hard to predict.

It rarely matters for a handful of objects per frame, and it matters a lot for the inner loop over thousands of particles or entities. Measure before you rewrite, then:

  • keep hot loops free of virtual calls: store plain structs by value in contiguous containers, one container per type, and loop over each;
  • call virtual once per batch (renderer.draw(span_of_sprites)), not once per element;
  • mark leaf classes final, which lets the compiler devirtualize calls it can see (cpp:interface --impl already generates a final class);
  • use std::variant where it fits: values in a std::vector, no heap, no pointer chasing, and a missing case is a compile error.

Patterns are a vocabulary, not a rule book. A pattern name describes a solution that worked in a particular context; it doesn't say your code needs it. Before reaching for one, ask:

  • What changes here, and who changes it? If nothing varies, nothing needs the indirection.
  • Is there a second implementation yet? Turning a plain struct into an interface later is easy. Removing an abstraction nobody uses is not.
  • What does it cost, and where? Runtime (the list above), compile time (templates), and reading time (every layer is one more file to open).

The Core Guidelines back the plain option more often than you might expect: use a struct for data without an invariant (C.2, cpp:struct), prefer concrete types over class hierarchies (C.10), and use a hierarchy only for concepts with inherent hierarchical structure (C.120). They are a neutral reference to point at in a design discussion, and they apply to patterns the same way they apply to everything else: "can you justify this?" is a fair question for a Factory and for a free function alike.

This is the core of the "clean code vs. performance" debate: hiding every decision behind small polymorphic objects reads nicely, but costs real throughput once it's in the hot path. Use virtual at the architectural seams (rendering backend, audio device, plugin boundary) and plain data in the loops.

Tooling

These work on an existing project made from a starter.

cpp:doctest

Sets up doctest via FetchContent: enable_testing(), a tests/ directory with a DOCTEST_CONFIG_IMPLEMENT_WITH_MAIN main, a tests target linked against ${PROJECT_NAME}-core, doctest_discover_tests(), and the # jen:tests marker for --withTest. Param: doctestTag (default v2.4.12).

$ jen cpp:doctest
cpp:doctest (pack @codejen/pack-cpp) – set up doctest: tests/ with a main, a tests target linked against <name>-core, ctest discovery, and the # jen:tests marker for --withTest

  + tests/CMakeLists.txt
  + tests/main.cpp
  ~ CMakeLists.txt  + enable_testing() (+1 more)

✓ 3 written

doctest also supports inline tests in the .cpp files with DOCTEST_CONFIG_DISABLE in release builds. That's charming for small projects but scales worse, so it isn't scaffolded.

Params

doctestTag

doctest git tag to fetch

default v2.4.12

cpp:tidy

A .clang-tidy enabling cppcoreguidelines-*, modernize-*, bugprone-* and performance-*, with the checks that are noisy in graphics/SDL code (magic numbers, pointer arithmetic, varargs, …) switched off and explained in a comment. It also adds cmake/tidy.cmake, an opt-in -DCLANG_TIDY=ON that runs it during the build; --cmake=false skips that part.

$ jen cpp:tidy
cpp:tidy (pack @codejen/pack-cpp) – add a .clang-tidy enforcing the C++ Core Guidelines, plus an opt-in CLANG_TIDY CMake option (cmake/tidy.cmake)

  + .clang-tidy
  + cmake/tidy.cmake
  ~ CMakeLists.txt  + include(cmake/tidy.cmake)

✓ 3 written

Params

cmake

also add the CLANG_TIDY CMake option (cmake/tidy.cmake); --cmake=false for just .clang-tidy

default true

cpp:format

A .clang-format matching the style the generators emit. Keep --indent and --columnLimit in line with your .editorconfig.

$ jen cpp:format
cpp:format (pack @codejen/pack-cpp) – add a .clang-format matching the generated code style (Google-based), aligned with .editorconfig

  + .clang-format

✓ 1 written

Params

basedOn

clang-format base style, e.g. LLVM, Chromium, Mozilla, WebKit

default Google

indent

indent width in spaces

default 2

columnLimit

maximum line length

default 120

cpp:presets

A CMakePresets.json with debug, release, asan and ubsan configure, build and test presets (cmake --preset asan). The sanitizer presets are for GCC and Clang.

$ jen cpp:presets
cpp:presets (pack @codejen/pack-cpp) – add a CMakePresets.json with debug, release, asan and ubsan presets

  + CMakePresets.json

✓ 1 written

cpp:warnings

A project_warnings INTERFACE target with -Wall -Wextra -Wpedantic -Wconversion -Wshadow (/W4 /permissive- on MSVC), linked privately into the core library so vendored code isn't affected. --werror adds -Werror / /WX.

$ jen cpp:warnings
cpp:warnings (pack @codejen/pack-cpp) – add a project_warnings INTERFACE target (-Wall -Wextra … resp. /W4) and link it into the core library

  + cmake/warnings.cmake
  ~ CMakeLists.txt  + include(cmake/warnings.cmake)
  ~ src/CMakeLists.txt  + target_link_libraries(${PROJECT_NAME}-core PRIVATE project_warnings)

✓ 3 written

Params

werror

treat warnings as errors (-Werror, /WX)

default false

cpp:compiler

Compiler settings that nudge the build towards the Core Guidelines, complementing cpp:warnings (general warnings) and cpp:tidy (static analysis). cmake/compiler.cmake sets build-wide conventions: CMAKE_CXX_EXTENSIONS OFF (-std=c++23, not gnu++23, P.2), CMAKE_CXX_STANDARD_REQUIRED, and compile_commands.json for clangd and clang-tidy. It also defines a project_options INTERFACE target, linked privately into the core library, with:

  • guideline warnings, each commented with its rule: -Wnon-virtual-dtor (C.35), -Woverloaded-virtual (C.138), -Wsuggest-override (C.128), -Wold-style-cast (ES.49), -Wcast-align (ES.48), -Wdouble-promotion (ES.46), -Wformat=2 (ES.34), -Wimplicit-fallthrough (ES.78), -Wnull-dereference (ES.65) and -Werror=return-type; /utf-8 /Zc:__cplusplus /Zc:inline /EHsc on MSVC
  • hardening: bounds-checked operator[] in Debug via _GLIBCXX_ASSERTIONS (and libc++'s hardening modes), -fstack-protector-strong, _FORTIFY_SOURCE=2 outside Debug, stack-clash protection and CET on Linux x86, /GS /guard:cf /sdl on MSVC. --hardening=false leaves these out.

-Wuseless-cast is deliberately not enabled: it fires inside SDL's own macros. Note that the Debug-only bounds checks are inactive without a build type, so use the debug preset or -DCMAKE_BUILD_TYPE=Debug.

$ jen cpp:compiler
cpp:compiler (pack @codejen/pack-cpp) – add guideline-oriented compiler settings (project_options: no extensions, guideline warnings, hardening) and link them into the core library

  + cmake/compiler.cmake
  ~ CMakeLists.txt  + include(cmake/compiler.cmake)
  ~ src/CMakeLists.txt  + target_link_libraries(${PROJECT_NAME}-core PRIVATE project_options)

✓ 3 written

Params

hardening

bounds-checked standard library and exploit mitigations (stack protector, fortify); --hardening=false to leave them out

default true

Profiling

cpp:tracy

The Tracy profiler via FetchContent, linked into the core library. A PROFILING CMake option (default OFF) drives TRACY_ENABLE; Tracy's macros compile to nothing without it, so no wrapper header is needed. Inserts FrameMark; at // jen:frame-end. --gpu (OpenGL starter) also adds TracyGpuContext; at // jen:gl-init and TracyGpuCollect; before the frame mark.

$ jen cpp:tracy --gpu
cpp:tracy (pack @codejen/pack-cpp) – add the Tracy profiler (PROFILING option, FetchContent) and a FrameMark at the app starter's frame end

  + cmake/tracy.cmake
  ~ CMakeLists.txt  + include(cmake/tracy.cmake)
  ~ src/CMakeLists.txt  + target_link_libraries(${PROJECT_NAME}-core PUBLIC Tracy::TracyClient)
  ~ src/app.cpp  + #include <tracy/Tracy.hpp>
  ~ src/app.cpp  + #include <tracy/TracyOpenGL.hpp>
  ~ src/app.cpp  + TracyGpuContext;
  ~ src/app.cpp  + TracyGpuCollect;
  ~ src/app.cpp  + FrameMark;

✓ 4 written

Params

gpu

also profile OpenGL on the GPU (GPU zones)

default false

file

the app source to hook into (default: the one with the starter's // jen:includes marker)

tracyTag

Tracy git tag to fetch

default v0.11.1

Then cmake -S . -B build -DPROFILING=ON.

Packaging

cpp:cpack

install() rules and CPack packaging, at # jen:install and # jen:cpack: the executable, an assets directory (skipped if it doesn't exist), CPACK_GENERATOR per platform (Windows ZIP;NSIS, macOS DragNDrop, Linux TGZ;DEB), an $ORIGIN RPATH so the unpacked binary finds its libraries, and a macOS .app bundle with cmake/Info.plist.in.

A shared SDL3 is shipped next to the executable (IMPORTED_RUNTIME_ARTIFACTS for an installed SDL3, install(TARGETS SDL3-shared) for a fetched one). --staticSdl builds SDL3 statically instead, often the simpler choice for games. The NSIS generator needs NSIS installed; cpack -G ZIP works without it.

$ jen cpp:cpack --staticSdl
cpp:cpack (pack @codejen/pack-cpp) – add install rules and CPack packaging (ZIP/NSIS, DragNDrop, TGZ/DEB), SDL3 runtime, RPATH and a macOS bundle

  + cmake/Info.plist.in
  ~ CMakeLists.txt  + set(SDL_STATIC ON) (+1 more)
  ~ CMakeLists.txt  + include(GNUInstallDirs) (+25 more)
  ~ CMakeLists.txt  + set(CPACK_PACKAGE_NAME "${PROJECT_NAME}") (+10 more)

✓ 2 written

Params

assets

folder to install next to the app, if it exists

default assets

staticSdl

link SDL3 statically instead of shipping its shared library

default false

bundleId

macOS bundle identifier (default: com.example.<project>)

maintainer

package vendor and Debian maintainer, e.g. "Jane Doe <jane@example.com>"

cpp:icon

Platform app resources, separate from embedding: a Windows resources/app.rc that attaches an icon to the executable, and a macOS .icns copied into the bundle's Resources, both wired in at # jen:app. jen only writes text, so drop your own resources/app.ico and resources/app.icns in (--ico and --icns change the paths). Pairs with cpp:cpack.

$ jen cpp:icon
cpp:icon (pack @codejen/pack-cpp) – wire a Windows .rc icon and a macOS .icns bundle icon into the executable (you supply the icon files)

  + resources/app.rc
  ~ src/CMakeLists.txt  + if(WIN32) (+6 more)

✓ 2 written

Params

ico

Windows icon, relative to the project root

default resources/app.ico

icns

macOS icon, relative to the project root

default resources/app.icns

Resources

cpp:shader

Embeds a GLSL file you already have: write it with glsl:frag from the glsl pack, or by hand. cpp:shader turns it into a <file>.h header holding the source as a C string at CMake-configure time, using cmake/embed-glsl.cmake (added if missing). It writes no GLSL of its own; it only wires embed_glsl(...) in at the # jen:embed marker, with the shader's path relative to that CMakeLists.txt. Edit the .glsl later and just re-run cmake.

$ jen cpp:shader --file=shaders/vignette.frag.glsl
cpp:shader (pack @codejen/pack-cpp) – embed an existing GLSL file as a C string via cmake/embed-glsl.cmake, wired in at # jen:embed (write the shader with glsl:frag or by hand)

  = cmake/embed-glsl.cmake  exists, skipped
  ~ src/CMakeLists.txt  + include(${PROJECT_SOURCE_DIR}/cmake/embed-glsl.cmake)
  ~ src/CMakeLists.txt  + embed_glsl("../shaders/vignette.frag.glsl" vignetteFragmentShader)

✓ 2 written

If the file doesn't exist yet, jen says so and points at glsl:frag.

Params

file

the GLSL file to embed, e.g. shaders/tonemap.frag.glsl

required

name

C++ variable name (default: from the file name, e.g. tonemapFragmentShader)

cpp:embed

Cross-platform binary embedding. #embed isn't portable yet (C only in Clang/GCC 15, C++26 for C++, no MSVC), so the byte array is generated at build time with add_custom_command: assets are re-embedded when they change, without re-running cmake. Then #include "tilesPng.h" and use SDL_IOFromConstMem(tilesPng.data(), tilesPng.size()).

$ jen cpp:embed --file=assets/tiles.png --name=tilesPng
cpp:embed (pack @codejen/pack-cpp) – embed a binary file as a constexpr byte array, generated at build time by cmake/embed.cmake and wired in at # jen:embed

  + cmake/embed-file.cmake
  + cmake/embed.cmake
  ~ src/CMakeLists.txt  + include(${PROJECT_SOURCE_DIR}/cmake/embed.cmake)
  ~ src/CMakeLists.txt  + embed_file(${PROJECT_NAME}-core assets/tiles.png tilesPng)

✓ 3 written

Params

file

the binary file to embed, relative to the project root

required

name

C++ variable name for the byte array

required

target

CMake target that gets the array (default: <project>-core)

method

how to embed: only cmake so far (#embed isn't portable yet)

default cmake

one of cmake

CMake's regex gets noticeably slow for assets in the tens of MB, and each generated header should be included from exactly one .cpp, since large constexpr arrays cost compile time in every translation unit.

Putting it together

The starters set up the markers the other generators insert into, so a starter plus scaffolding is just a few commands:

$ jen cpp:sdl3-opengl --name=MyGame
…
$ jen glsl:frag --name=vignette --version=410
…
$ jen cpp:shader --file=shaders/vignette.frag.glsl
…
$ jen cpp:class --name=Camera
…
$ cmake -S . -B build && cmake --build build

And with the tooling on top:

$ jen cpp:doctest && jen cpp:warnings && jen cpp:compiler && jen cpp:tidy
…
$ jen cpp:class --name=Camera --withTest
…
$ jen cpp:enum --name=GameState --values="menu, playing, paused" --withTest
…
$ jen cpp:tracy --gpu
…
$ cmake -S . -B build && cmake --build build && ctest --test-dir build

C++ standard

The starters build as C++23 (cxx_std_23), which cpp:enum's std::unreachable() and std::format formatter rely on. cpp:enum --std=20 is there for projects that stay on C++20.