A libclang/libtooling-based C++ introspection engine. It parses real C++23 with
a Clang frontend and, from a single AST walk, emits pybind11/nanobind binding
TUs, .pyi type stubs, Sphinx C++ API documentation (cpp/c-domain directives,
including macros — no Doxygen), and a structured public-API JSON IR.
Apiary began as apiary, the binding generator for the
Einsums tensor library, and is being
generalized into a standalone tool. The annotation contract still uses the
APIARY_* macro names (shipped in include/apiary/Annotations.hpp);
these will be renamed to neutral APIARY_* names in a later pass.
Walking Einsums headers, it finds declarations marked with APIARY_*
macros and emits two artifacts per annotated module:
- A pybind11 binding TU that gets linked into a single
einsumsPython extension, so users get oneimport einsumsinstead of one import per module. - A
.pyitype-stub fragment for that module. After every module has been processed, a small Python aggregator merges fragments into per-submodule stubs (einsums/_core.pyi,einsums/linalg.pyi,einsums/graph.pyi, …) that pyright / mypy consume.
The tool is gated on the EINSUMS_BUILD_PYTHON CMake option (default
OFF). When the option is ON, modules opt in by passing PYBIND to
einsums_add_module(), and the rest — bindings + stubs — happens
automatically.
Annotate the C++ class you want to bind:
// libs/Einsums/MyModule/include/Einsums/MyModule/Greeter.hpp
#include <Einsums/Python/Annotations.hpp>
namespace einsums::mymodule {
class APIARY_EXPOSE Greeter {
public:
APIARY_EXPOSE
Greeter();
APIARY_EXPOSE
explicit Greeter(std::string greeting);
APIARY_EXPOSE
std::string say(std::string const &name) const;
};
} // namespace einsums::mymoduleOpt the module into autogen:
# libs/Einsums/MyModule/CMakeLists.txt
einsums_add_module(
Einsums MyModule
PYBIND # <-- this line
SOURCES Greeter.cpp
HEADERS Einsums/MyModule/Greeter.hpp
MODULE_DEPENDENCIES Einsums_Config
)Configure with Python enabled, build, import:
cmake -S . -B build -DEINSUMS_BUILD_PYTHON=ON
cmake --build build --target PyEinsums
PYTHONPATH=build/lib python3 -c "
import einsums
g = einsums.Greeter('hi')
print(g.say('world'))
"That's it. The codegen runs as a build edge (re-fires on header
changes), and the resulting bindings end up alongside every other
annotated module under one import einsums.
Every macro is a C++11 attribute that places between the class-key and the class name (or before a function return type). Multiple macros stack. Under non-Clang compilers all macros expand to nothing, so production builds carry no overhead.
| Macro | Purpose |
|---|---|
APIARY_EXPOSE |
Mark a declaration for binding. Without this, the codegen ignores it. |
APIARY_HIDE |
Suppress binding for an otherwise-exposed declaration (e.g. an inherited member). |
| Macro | Purpose |
|---|---|
APIARY_RENAME("py_name") |
Override the Python identifier used for the binding. |
APIARY_MODULE("submodule") |
Place the binding inside a Python submodule. Dotted names ("tensor.algebra") request nested submodules. |
APIARY_EXCEPTION |
Bind the class as a Python exception via py::register_exception<T> instead of py::class_<>. C++ class must derive from std::exception (or compatible). pybind11-only. |
| Macro | Purpose |
|---|---|
APIARY_HOLDER(std::shared_ptr) |
Override the pybind11 holder type. Default is std::unique_ptr. |
APIARY_BUFFER_PROTOCOL |
Flip on pybind11's buffer protocol. Pair with BUFFER_FROM. |
APIARY_BUFFER_FROM(helper) |
Free function helper(T&) returning py::buffer_info; codegen wraps it in a .def_buffer() lambda. |
APIARY_IMPLICIT_FROM(Source) |
Emit py::implicitly_convertible<Source, Class>() after the binding. |
APIARY_DYNAMIC_ATTR |
Allow Python instances to carry arbitrary attributes. |
APIARY_NOCOPY |
Skip generation of the copy-ctor binding. |
APIARY_NOMOVE |
Skip generation of the move-ctor binding. |
APIARY_NO_BASES |
Force-skip emission of base-class arguments. Usually unnecessary — the emitter auto-skips bases that aren't themselves bound. |
APIARY_READONLY |
On a field — bind as def_readonly instead of def_readwrite. |
| Macro | Purpose |
|---|---|
APIARY_RVP(reference_internal) |
Set return_value_policy. Argument is the unqualified policy name. |
APIARY_KEEP_ALIVE(0, 1) |
Emit py::keep_alive<nurse, patient>(). |
APIARY_RELEASE_GIL |
Wrap the call in py::call_guard<py::gil_scoped_release>(). |
APIARY_OPERATOR("__add__") |
Bind the method as a Python operator instead of a named function. |
APIARY_GETTER("name") and APIARY_SETTER("name") get
merged into one .def_property("name", &get, &set) when the codegen
sees a matching name on a getter/setter pair. A @getter with no
matching @setter becomes a .def_property_readonly.
Doxygen comments (/// or /** */) above an exposed declaration become
the Python docstring automatically. Override explicitly with
APIARY_DOC("text").
Templated classes need an explicit instantiation directive — pybind11 binds concrete types, not templates.
Cross-product (APIARY_INSTANTIATE): each parameter list
is keyed by the exact C++ template-parameter name. The codegen matches
by name, not position, so the order in the macro is free. Python names
are auto-derived from the values.
template <typename T, int rank>
class APIARY_EXPOSE
APIARY_INSTANTIATE(Tensor,
T(float, double),
rank(1, 2))
Tensor { ... };
// Produces: Tensor_float_1, Tensor_float_2, Tensor_double_1, Tensor_double_2Single instantiation (APIARY_INSTANTIATE_AS): pin one
concrete type to a chosen Python name. Use this when one template
parameter depends on another (e.g. Alloc = std::allocator<T>), which
a flat cross-product can't express.
APIARY_INSTANTIATE_AS("Tensor2d_double",
GeneralTensor<double, 2, std::allocator<double>>)Cross-product with name template (APIARY_INSTANTIATE_TEMPLATE):
same matching rules; placeholders in the name template use the C++
template-parameter names too.
template <typename Element, int Rank>
class APIARY_EXPOSE
APIARY_INSTANTIATE_TEMPLATE("Block_{Element}_{Rank}",
Block,
Element(float, double),
Rank(1, 2))
Block { ... };
// Produces: Block_float_1, Block_float_2, Block_double_1, Block_double_2Placeholder values are sanitized to valid Python identifiers
(std::complex<double> → std_complex_double).
APIARY_INSTANTIATE_AS also works on templated free functions —
each directive defines one instantiation. Multiple directives sharing a
Python name turn into a pybind11 overload set; the codegen picks the
right one at call site via Python's argument types.
template <typename T>
APIARY_EXPOSE
APIARY_INSTANTIATE_AS("scale", einsums::RuntimeTensor<float>)
APIARY_INSTANTIATE_AS("scale", einsums::RuntimeTensor<double>)
void scale(typename T::ValueType factor, T *A);When two or more INSTANTIATE_AS lines share a Python name AND their
argument signatures are identical (only the return type or value-type
differs), the codegen automatically collapses them into a single Python
entry that takes a dtype="..." kwarg and dispatches at runtime:
template <typename T>
APIARY_EXPOSE
APIARY_INSTANTIATE_AS("create_zero_tensor", float)
APIARY_INSTANTIATE_AS("create_zero_tensor", double)
APIARY_INSTANTIATE_AS("create_zero_tensor", std::complex<float>)
APIARY_INSTANTIATE_AS("create_zero_tensor", std::complex<double>)
RuntimeTensor<T> create_zero_tensor(std::string name, std::vector<size_t> dims);
// Python: create_zero_tensor("X", [4, 4], dtype="float64")Recognized dtype aliases (numpy convention): float32/f4/f/single
(float), float64/f8/d (double), complex64/c8/F
(complex), complex128/complex/c16/D (complex).
The default dtype is float64 if double is in the group, otherwise
the first instantiation's first alias.
For functions templated on leading bool parameters (e.g. template <bool TransA, bool TransB, typename T>), pair
APIARY_TEMPLATE_KWARGS with APIARY_INSTANTIATE_BOOLS.
The codegen expands 2^N combinations internally and emits one Python
entry per dtype taking each bool as a kw-only argument:
template <bool TransA, bool TransB, typename T>
APIARY_EXPOSE
APIARY_TEMPLATE_KWARGS("trans_a", "trans_b")
APIARY_INSTANTIATE_BOOLS("gemm", einsums::RuntimeTensor<float>, float)
APIARY_INSTANTIATE_BOOLS("gemm", einsums::RuntimeTensor<double>, double)
void gemm(U alpha, T const &A, T const &B, U beta, T *C);
// Python: gemm(1.0, A, B, 0.0, C, trans_a=True, trans_b=False)The first INSTANTIATE_BOOLS argument is the Python name (shared
across the bool fan-out); the rest are the non-bool template args.
The 2^N bool combinations are generated automatically; the codegen
then emits a single Python def per dtype with an internal if-chain
dispatcher.
Use APIARY_INSTANTIATE_MEMBER_AS to bind a templated method
with its own template parameters (independent of the enclosing class's
parameters). Multiple directives stack; same-signature ones with
recognized dtypes auto-merge into a dtype= dispatcher exactly like
free-function INSTANTIATE_AS.
template <typename T>
class APIARY_EXPOSE Workspace { ... };
class Workspace {
template <typename U>
APIARY_EXPOSE
APIARY_INSTANTIATE_MEMBER_AS("declare_runtime_tensor",
U=einsums::RuntimeTensor<float>)
APIARY_INSTANTIATE_MEMBER_AS("declare_runtime_tensor",
U=einsums::RuntimeTensor<double>)
U &declare_runtime_tensor(std::string name, std::vector<size_t> dims);
};APIARY_INSTANTIATE_MEMBER (no _AS) is the same idea for
members whose own parameters depend on the class's. Argument is a
Name=Type pair like Dim=std::vector<size_t>.
Templated classes often have parameter-pack constructors whose arity depends on a template parameter. The codegen needs to know how many arguments to bind per instantiation.
template <typename T, size_t rank>
struct APIARY_EXPOSE
APIARY_INSTANTIATE_AS("Tensor_double_2",
GeneralTensor<double, 2, std::allocator<double>>)
GeneralTensor {
template <typename... Dims>
APIARY_EXPOSE
APIARY_VARIADIC_FROM(rank, size_t) // pack -> rank-many size_t args
GeneralTensor(std::string name, Dims... dims);
};For Tensor_double_2, this binds a ctor with signature
(std::string, size_t, size_t). For Tensor_double_3 it would be
(std::string, size_t, size_t, size_t).
The first arg names the template parameter that gives the count; the second is the concrete C++ type each expanded slot should take. The last function parameter is assumed to be the pack.
The codegen emits Clang-style file:line errors and exits non-zero on problems. Common cases:
| Error | Cause |
|---|---|
unknown parameter keyword 'X' (template parameters are: ...) |
Either a typo, or an upstream #define mangled the keyword before stringification. |
class name '<X>' in directive payload does not match |
Same cause: macro expansion changed the class name token. |
expected N parameter list(s), got M |
Number of Param(...) groups doesn't match the template signature. |
parameter keyword '<X>' specified more than once |
Duplicate group. |
missing parameter list for template parameter '<X>' |
A template parameter has no matching group. |
The strict name-match for INSTANTIATE / INSTANTIATE_TEMPLATE is the
load-bearing guard against random macro expansion. If some upstream
header has #define Element WHATEVER, the codegen sees WHATEVER(...),
fails the match against the real Element parameter, and emits a
diagnostic instead of producing wrong bindings.
-
Configure —
einsums_finalize_pybind()runs at the end of root configure. It writes the aggregator main (PyEinsumsMain.cpp, prototypes +PYBIND11_MODULE(einsums, m)calling each module's register function), creates thePyEinsumspybind11_add_moduletarget, and emits oneadd_custom_commandper opted-in module. -
Build — ninja resolves the dependency chain:
apiarytool builds first.- Each
Einsums_<Module>library builds. - For every annotated module,
apiaryruns over its headers and emits${BUILD}/generated/pybind/Einsums_<Module>_pybind.cpp, which contains avoid apiary_register_<Module>(py::module_ &m)function. - The aggregator main and every generated TU compile.
PyEinsumslinks them all into${BUILD}/lib/einsums.cpython-*.so.
Touching an annotated header re-fires only that module's codegen edge
(via the add_custom_command's DEPENDS ${headers}) and re-links the
single shared library.
- Default constructors with conditional
requiresclauses that are compile-timedeleted for some instantiations may emit spurious bindings. UseAPIARY_HIDEto suppress per-method. - Cross-product with dependent parameters can't be expressed
(
Alloc = std::allocator<T>). Fall back to oneAPIARY_INSTANTIATE_ASper concrete type. - System header detection assumes Clang's
-print-resource-diris available and (on macOS)xcrun --show-sdk-path. The condaeinsums-devenv satisfies both. Other setups may need to setAPIARY_CLANG_RESOURCE_DIR/APIARY_SYSROOTmanually before the first configure. requires requires { … }clauses block doxygen attachment — clang'sgetRawCommentForDecldoesn't associate///comments with a function template that has a nested requires-expression. Flatten to a singlerequires (A && B && …)clause and the comment (and thus the Python docstring) will attach.- Stub-side metafunction expansion isn't supported — return types
involving
RemoveComplexT<T>and friends fall back toAnyin the generated.pyi. The runtime binding is correct; only the static type information is reduced.
apiary can emit code against either pybind11 (default) or
nanobind. Pass --target {pybind11,nanobind} on the command line:
apiary --target nanobind --module myext header.hpp -- ...The output differs in:
- Headers —
<pybind11/...>vs<nanobind/...>, with nanobind's STL bindings split per-type (<nanobind/stl/string.h>,<nanobind/stl/vector.h>, etc.) - Module macro —
PYBIND11_MODULEvsNB_MODULE - Namespace —
py::vsnb:: - Return value policy —
py::return_value_policy::reference_internalvsnb::rv_policy::reference_internal - Buffer protocol — pybind11 emits
.def_buffer()lambdas; nanobind doesn't have an equivalent directive (usenb::ndarray<>for tensor protocol instead).APIARY_BUFFER_FROMdirectives are silently dropped under the nanobind target.
The einsums_finalize_pybind CMake integration uses pybind11 today.
Switching the autogen pipeline to nanobind requires also swapping
pybind11_add_module for nanobind_add_module and the matching
find_package(nanobind). The --target flag is what makes the rest of
that switch a one-line change in the cmake hook.
Every codegen invocation also produces a Python type-stub fragment,
emitted alongside the generated .cpp:
build/generated/pybind/Einsums_LinearAlgebra_pybind.cpp # bindings
build/generated/pybind/Einsums_LinearAlgebra.pyi # stub fragment
A finalize step (tools/apiary/scripts/aggregate_stubs.py)
runs as a PyEinsumsStubs custom target after PyEinsums is linked.
It splits each fragment by the # %%submodule: <name> sentinels the
emitter inserts and merges them into per-submodule files in the
package directory:
build/lib/einsums/
├── _core.cpython-…so # the C extension
├── _core.pyi # top-level entities
├── linalg.pyi # entities tagged @module("linalg")
├── graph.pyi # entities tagged @module("graph")
├── __init__.py / .pyi # runtime + stub re-exporting _core
└── py.typed # PEP 561 marker
Type translation runs per-instantiation:
# scale (free function with INSTANTIATE_AS for four dtypes)
@overload
def scale(factor: float, A: RuntimeTensorF) -> None: ...
@overload
def scale(factor: float, A: RuntimeTensorD) -> None: ...
@overload
def scale(factor: complex, A: RuntimeTensorC) -> None: ...
@overload
def scale(factor: complex, A: RuntimeTensorZ) -> None: ...
# create_zero_tensor (auto-detected dtype dispatcher)
def create_zero_tensor(name: str, dims: list[int], dtype: str = "float64") \
-> RuntimeTensorF | RuntimeTensorD | RuntimeTensorC | RuntimeTensorZ: ...
# gemm (TEMPLATE_KWARGS bool fan-out)
@overload
def gemm(alpha: float, A: RuntimeTensorF, B: RuntimeTensorF, beta: float,
C: RuntimeTensorF, *, trans_a: bool = False, trans_b: bool = False) -> None: ...
@overload
def gemm(alpha: complex, A: RuntimeTensorC, B: RuntimeTensorC, beta: complex,
C: RuntimeTensorC, *, trans_a: bool = False, trans_b: bool = False) -> None: ...Doxygen comments above an exposed declaration become Python docstrings.
@getter / @setter pairs become @property declarations.
Rich-comparison dunders (__eq__, __lt__, …) are widened to take
object to satisfy LSP — a stub typed __eq__(self, other: Vec3)
would otherwise trip pyright's reportIncompatibleMethodOverride.
When a function in one module takes a tensor from another module
(cg::scale taking RuntimeTensor<T> defined in the Tensor module),
the visitor records the external annotated class with is_external=true
purely for name resolution. The C++ emitter ignores externals (their
binding lives in the owning module's TU); the .pyi emitter uses
them to map GeneralRuntimeTensor<float, std::allocator<float>> →
RuntimeTensorF so cross-module signatures resolve without needing a
shared registry across codegen invocations.
For each per-instantiation parameter / return type, the .pyi emitter:
- Substitutes template names on the raw C++ type (preserving forms
like
typename T::ValueTypefor re-resolution). - Tries the canonical (typedef-expanded) form via clang's
getCanonicalType()if the as-written form fails — catches alias templates likeRuntimeTensor<T>↔GeneralRuntimeTensor<T, std::allocator<T>>. - Inlines
typename Class<args>::ValueTypereferences withargs.first(Einsums tensor convention). - Substitutes any known cpp_to_py-mapped class instantiation in
nested types (so
std::tuple<RuntimeTensor<float>, ...>reduces totuple[RuntimeTensorF, ...]). - Falls back to
Anywhen none of the above produces a Python-valid identifier — pyright will surface the gap rather than the stub silently mistyping.
Each Python submodule needs a tiny .py shell next to _core.so. The
recommended pattern uses PEP 562 __getattr__ so the C extension
isn't loaded until first attribute access:
# einsums/graph.py
import importlib as _importlib
def __getattr__(name):
if name.startswith("_"):
raise AttributeError(name)
core = _importlib.import_module("._core.graph", "einsums")
attr = getattr(core, name)
globals()[name] = attr # cache for subsequent lookups
return attrThe generated <sub>.pyi describes the static surface; the .py shell
is just a runtime trampoline.
Annotated headers can use #if/#else/#endif against any
configure-time define, including everything that
einsums_add_config_define() writes into <Einsums/Config.hpp>. The
codegen tool runs Clang's full preprocessor and only sees the active
branch:
#include <Einsums/Config.hpp>
#include <Einsums/GPU/DeviceVector.hpp>
template <typename T, size_t rank, typename Alloc>
struct APIARY_EXPOSE
APIARY_INSTANTIATE_AS("Tensor_double_2",
GeneralTensor<double, 2, std::allocator<double>>)
#if defined(EINSUMS_HAVE_GPU)
APIARY_INSTANTIATE_AS("Tensor_double_2_gpu",
GeneralTensor<double, 2, gpu::DeviceAllocator<double>>)
#endif
GeneralTensor { ... };When EINSUMS_WITH_GPU=ON, the GPU instantiation is added; toggling it
off and reconfiguring drops it. The generated Defines.hpp files are
in every codegen edge's DEPENDS, so re-configure → re-fire codegen
automatically. Also forwarded: every INTERFACE_COMPILE_DEFINITIONS
reachable from the module's MODULE_DEPENDENCIES (gets -D flags on the
codegen invocation).
- Visibility warnings when linking PyEinsums (weak symbols across the per-module library and the generated TU) are cosmetic on macOS; symbols still resolve correctly.
src/
main.cpp CLI driver: ClangTool + per-TU IR accumulation +
emit pass. Drives the post-IR passes
(compute_python_overloads, compute_properties)
before invoking the C++ and .pyi emitters.
Tracks total error count, exits non-zero.
Visitor.hpp/.cpp RecursiveASTVisitor that walks declarations,
filters by apiary: annotation, builds
the Module IR. Captures annotated classes from
outside the current module's headers as
``is_external`` for cross-module name resolution.
IR.hpp/.cpp BoundClass / BoundMethod / BoundField / BoundEnum /
BoundFunction / BoundParam / BoundInstantiation /
BoundProperty / PythonOverload. Plus a
deterministic textual dump (``--dump-ir``).
AnnotationParser.hpp/.cpp
Splits raw "apiary:<directive>:<args>"
payloads into structured Directive records.
Knows about free-form-tail directives (doc,
instantiate, holder) where the tail may contain
':'.
InstantiateParser.hpp/.cpp
Parses INSTANTIATE / INSTANTIATE_TEMPLATE
payloads into ParamGroup lists, respects nested
``<>`` and ``()``. Provides cross_product and
sanitize_python_name helpers.
TypeTranslator.hpp/.cpp
Wraps Clang's PrintingPolicy for fully-qualified
pretty C++ types. Also provides
``translate_python_type`` (and the string-only
variant ``translate_python_type_string``) that
maps fundamentals + std containers to their
Python equivalents.
DocExtractor.hpp/.cpp
Pulls doxygen text from
ASTContext::getRawCommentForDeclNoCache, strips
leading ``///``/``*`` markers and decoration
banner lines (``//////``, ``=====``, ``-----``).
PythonOverloads.hpp/.cpp
Post-IR pass that decides how each free
function's raw instantiation list collapses
into Python entries: NonTemplate,
SingleInstantiation, OverloadSet,
DtypeDispatcher, TemplateKwargsDispatcher.
Both emitters consume the precomputed view.
Properties.hpp/.cpp Post-IR pass that walks each class's methods
and collapses @getter/@setter pairs into
BoundClass.properties.
Emitter.hpp/.cpp IR -> pybind11 C++ (or nanobind, via
``--target``). Two output modes:
PYBIND11_MODULE(name, m) (standalone fixtures /
goldens) or void register_<Module>(py::module_ &m)
(autogen aggregator path). In-process
clang::format::reformat() picks up the project's
.clang-format.
PyiEmitter.hpp/.cpp IR -> Python .pyi stubs. Walks the same IR plus
the post-pass views, emits per-submodule blocks
delimited by ``# %%submodule: <name>`` sentinels
for the aggregator to split.
scripts/
aggregate_stubs.py Reads every ``*.pyi`` fragment in
``--frag-dir`` and merges by submodule sentinel
into per-submodule files in ``--pkg-dir``.
Writes a shared header per output and the
PEP-561 ``py.typed`` marker.
tests/
fixtures/ Annotated headers used by the emitter tests.
golden/ Expected emitter output (regen with REGEN=1).
run_smoke.sh IR-dump substring assertions covering
BoundClass/BoundMethod/BoundFunction shapes,
property merge, submodule routing, Python
type translation, and default-value sanitization.
run_golden.sh pybind11 emitter golden-file diff.
- Define the macro in
libs/Einsums/Python/include/Einsums/Python/Annotations.hpp. - If its tail can contain
:, add it todirective_takes_free_form_tailinAnnotationParser.cpp. - Handle it in the emitter (most directives are read via
DirectiveViewinEmitter.cpp's class-body / method-emission helpers). - Add a fixture under
tests/fixtures/and regenerate goldens (REGEN=1 tests/run_golden.sh ...). - Exercise it end-to-end in
libs/Einsums/PythonDemo/CMakeLists.txt'sapiary_python_smokectest entry so the Python side is verified.
class APIARY_EXPOSE Resource {
public:
/// Read-only-from-Python access to the underlying name.
APIARY_GETTER("name")
std::string const &get_name() const;
/// Pythonic name setter.
APIARY_SETTER("name")
void set_name(std::string const &n);
};Generated stub:
class Resource:
@property
def name(self) -> str:
"""Read-only-from-Python access to the underlying name."""
...
@name.setter
def name(self, value: str) -> None: ...class APIARY_EXPOSE Vec3 {
public:
/// Component-wise equality.
APIARY_EXPOSE APIARY_OPERATOR("__eq__")
bool operator==(Vec3 const &other) const;
};Generated stub (note the object widening for LSP compliance):
class Vec3:
def __eq__(self, other: object) -> bool:
"""Component-wise equality."""
...namespace APIARY_MODULE("graph") cg {
APIARY_EXPOSE
class Graph { ... };
APIARY_EXPOSE
void execute(Graph &g);
} // namespace cgBoth Graph and execute end up in einsums.graph. The aggregator
writes them into build/lib/einsums/graph.pyi. Anything outside the
namespace block (or any entity tagged with its own
APIARY_MODULE("…")) routes to the chosen submodule.
#include <Einsums/Config.hpp>
APIARY_EXPOSE
APIARY_INSTANTIATE_AS("Tensor_double_2",
GeneralTensor<double, 2, std::allocator<double>>)
#if defined(EINSUMS_HAVE_GPU)
APIARY_INSTANTIATE_AS("Tensor_double_2_gpu",
GeneralTensor<double, 2, gpu::DeviceAllocator<double>>)
#endif
template <typename T, size_t rank, typename Alloc>
class GeneralTensor { ... };Toggle EINSUMS_WITH_GPU and reconfigure — the codegen picks up the
Defines.hpp mtime change and re-fires automatically; the GPU
instantiation appears (or disappears) in the generated bindings + stubs.