From d6be76946e29c5584ac0dddd20fd5ea9d8fd423c Mon Sep 17 00:00:00 2001 From: Susi Lehtola Date: Sat, 29 Aug 2026 18:38:14 +0300 Subject: [PATCH] Make header-only builds work from any number of translation units The runtime generator's definitions in generators/impl/ had external linkage, so including impl.hpp from two translation units produced duplicate symbols. The README documented the constraint honestly -- include it "exactly once per project" -- but that is not something a downstream project can enforce across its own dependencies. Marking the definitions unconditionally inline does not work either: an inline function that is not odr-used in a translation unit is never emitted, so the precompiled libintegratorxx would export nothing and the default build would fail to link. The two modes genuinely want different linkage, and the whole point of the precompiled default is to keep the large angular dispatch out of consumer translation units. Give them different linkage explicitly: * INTEGRATORXX_GENERATOR_LINKAGE (impl/linkage.hpp) expands to `inline` when INTEGRATORXX_HEADER_ONLY is defined and to nothing otherwise. * The CMake header-only branch now defines INTEGRATORXX_HEADER_ONLY on the interface target; previously the option only changed the target type, so nothing in the sources could tell the modes apart. * In header-only mode the public generator headers include their own implementations at the end of the file, where the declarations are already visible. The impl headers include the public ones back, which #pragma once reduces to a no-op. * test/lib_impl.cxx existed only to instantiate the generator once for the header-only test build, and is no longer needed. impl.hpp had no include guard at all; add one. Verified both ways: the default build still exports the generator symbols from libintegratorxx.a and passes 9/9 tests, the header-only build passes 9/9 tests, and two translation units that both include impl.hpp now link. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01FDTFYJMQ76iujDFNHzZyXF --- CMakeLists.txt | 1 + README.md | 19 ++++++++++--------- include/integratorxx/generators/impl/impl.hpp | 2 ++ .../integratorxx/generators/impl/linkage.hpp | 18 ++++++++++++++++++ .../generators/impl/pruned_grid.hpp | 5 +++-- .../generators/impl/radial_factory.hpp | 5 +++-- .../generators/impl/robust_pruning.hpp | 5 +++-- .../generators/impl/s2_factory.hpp | 5 +++-- .../generators/impl/treutler_pruning.hpp | 5 +++-- .../generators/impl/unpruned_grid.hpp | 3 ++- .../generators/radial_factory.hpp | 7 +++++++ .../integratorxx/generators/s2_factory.hpp | 7 +++++++ .../generators/spherical_factory.hpp | 8 ++++++++ test/CMakeLists.txt | 3 --- test/lib_impl.cxx | 2 -- 15 files changed, 70 insertions(+), 25 deletions(-) create mode 100644 include/integratorxx/generators/impl/linkage.hpp delete mode 100644 test/lib_impl.cxx diff --git a/CMakeLists.txt b/CMakeLists.txt index c530923..da1818c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -8,6 +8,7 @@ option(INTEGRATORXX_HEADER_ONLY "Force header-only build" OFF) if(INTEGRATORXX_HEADER_ONLY) add_library( integratorxx INTERFACE ) set(INTEGRATORXX_TARGET_TYPE INTERFACE) + target_compile_definitions( integratorxx INTERFACE INTEGRATORXX_HEADER_ONLY ) else() add_subdirectory(src) set(INTEGRATORXX_TARGET_TYPE PUBLIC) diff --git a/README.md b/README.md index d416142..9ab0a47 100644 --- a/README.md +++ b/README.md @@ -152,15 +152,16 @@ avoid excessive build times in complex projects with aggressive compiler optimization. **N.B. it is highly recommend that users maintain this default behavior to avoid excessive compilation sizes and build times**. -IntegratorXX also allows for header-only use of the runtime generator by -setting `INTEGRATORXX_HEADER_ONLY=ON`. -This feature also allows for circumvention of -the CMake build system by simply including the requisite implementation -header. - -To use the runtime generator header-only, one needs to include -`` **exactly once** per project, -otherwise duplicate / incompatible symbols will occur. +IntegratorXX also allows for header-only use of the runtime generator by +setting `INTEGRATORXX_HEADER_ONLY=ON`, which defines the +`INTEGRATORXX_HEADER_ONLY` macro on the interface target. The public +generator headers then carry their own implementations, so no additional +include is required. + +To circumvent the CMake build system entirely, define +`INTEGRATORXX_HEADER_ONLY` yourself and include the generator headers as +usual. The implementations have inline linkage in this mode and may be +included from any number of translation units. ## Contributing and Bug Reports diff --git a/include/integratorxx/generators/impl/impl.hpp b/include/integratorxx/generators/impl/impl.hpp index f8b061d..a71c36a 100644 --- a/include/integratorxx/generators/impl/impl.hpp +++ b/include/integratorxx/generators/impl/impl.hpp @@ -1,3 +1,5 @@ +#pragma once + #include #include #include diff --git a/include/integratorxx/generators/impl/linkage.hpp b/include/integratorxx/generators/impl/linkage.hpp new file mode 100644 index 0000000..ebb8240 --- /dev/null +++ b/include/integratorxx/generators/impl/linkage.hpp @@ -0,0 +1,18 @@ +#pragma once + +/** + * @brief Linkage of the runtime grid generator. + * + * By default the runtime generator is compiled once into libintegratorxx + * and consumers see only the declarations in generators/*.hpp. This keeps + * the large angular grid dispatch out of every translation unit. + * + * When INTEGRATORXX_HEADER_ONLY is defined the same definitions are emitted + * inline in each translation unit that uses them, and the public generator + * headers pull in their own implementations. + */ +#ifdef INTEGRATORXX_HEADER_ONLY +#define INTEGRATORXX_GENERATOR_LINKAGE inline +#else +#define INTEGRATORXX_GENERATOR_LINKAGE +#endif diff --git a/include/integratorxx/generators/impl/pruned_grid.hpp b/include/integratorxx/generators/impl/pruned_grid.hpp index c69e036..032be58 100644 --- a/include/integratorxx/generators/impl/pruned_grid.hpp +++ b/include/integratorxx/generators/impl/pruned_grid.hpp @@ -1,5 +1,6 @@ #pragma once +#include #include #include @@ -58,7 +59,7 @@ auto make_pruned_grid(const RadialQuadType& rq, } // Implementation Details -SphericalGridFactory::spherical_grid_ptr +INTEGRATORXX_GENERATOR_LINKAGE SphericalGridFactory::spherical_grid_ptr SphericalGridFactory::generate_pruned_grid( RadialQuad rq, const RadialTraits& traits, const std::vector& pruning_regions) { @@ -83,7 +84,7 @@ SphericalGridFactory::spherical_grid_ptr } -PrunedSphericalGridSpecification create_pruned_spec( +INTEGRATORXX_GENERATOR_LINKAGE PrunedSphericalGridSpecification create_pruned_spec( PruningScheme scheme, UnprunedSphericalGridSpecification unp ) { diff --git a/include/integratorxx/generators/impl/radial_factory.hpp b/include/integratorxx/generators/impl/radial_factory.hpp index b1c01a1..e2f45ad 100644 --- a/include/integratorxx/generators/impl/radial_factory.hpp +++ b/include/integratorxx/generators/impl/radial_factory.hpp @@ -1,4 +1,5 @@ #pragma once +#include #include #include @@ -7,7 +8,7 @@ namespace IntegratorXX { -RadialQuad radial_from_string(std::string name) { +INTEGRATORXX_GENERATOR_LINKAGE RadialQuad radial_from_string(std::string name) { std::transform(name.begin(), name.end(), name.begin(), [](unsigned char c){ return static_cast(std::toupper(c)); }); if(name == "BECKE") return RadialQuad::Becke; @@ -21,7 +22,7 @@ RadialQuad radial_from_string(std::string name) { throw std::runtime_error("Unrecognized Radial Quadrature"); } -RadialFactory::radial_grid_ptr RadialFactory::generate(RadialQuad rq, const RadialTraits& traits) { +INTEGRATORXX_GENERATOR_LINKAGE RadialFactory::radial_grid_ptr RadialFactory::generate(RadialQuad rq, const RadialTraits& traits) { switch(rq) { case RadialQuad::Becke: diff --git a/include/integratorxx/generators/impl/robust_pruning.hpp b/include/integratorxx/generators/impl/robust_pruning.hpp index f397fea..3ad7b87 100644 --- a/include/integratorxx/generators/impl/robust_pruning.hpp +++ b/include/integratorxx/generators/impl/robust_pruning.hpp @@ -1,4 +1,5 @@ #pragma once +#include #include #include @@ -24,7 +25,7 @@ auto get_robust_low_med_sizes(AngularSize asz) { } -PrunedSphericalGridSpecification robust_psi4_pruning_scheme_impl( +INTEGRATORXX_GENERATOR_LINKAGE PrunedSphericalGridSpecification robust_psi4_pruning_scheme_impl( size_t low_sz, size_t med_sz, AngularQuad angular_quad, UnprunedSphericalGridSpecification unp ) { @@ -46,7 +47,7 @@ PrunedSphericalGridSpecification robust_psi4_pruning_scheme_impl( } // Implementation Details -PrunedSphericalGridSpecification robust_psi4_pruning_scheme( +INTEGRATORXX_GENERATOR_LINKAGE PrunedSphericalGridSpecification robust_psi4_pruning_scheme( UnprunedSphericalGridSpecification unp ) { size_t low_sz, med_sz; diff --git a/include/integratorxx/generators/impl/s2_factory.hpp b/include/integratorxx/generators/impl/s2_factory.hpp index 3e0363d..7a0d20e 100644 --- a/include/integratorxx/generators/impl/s2_factory.hpp +++ b/include/integratorxx/generators/impl/s2_factory.hpp @@ -1,4 +1,5 @@ #pragma once +#include #include #include @@ -7,7 +8,7 @@ namespace IntegratorXX { -AngularQuad angular_from_string(std::string name) { +INTEGRATORXX_GENERATOR_LINKAGE AngularQuad angular_from_string(std::string name) { std::transform(name.begin(), name.end(), name.begin(), [](unsigned char c){ return static_cast(std::toupper(c)); }); if(name == "AHRENSBEYLKIN") return AngularQuad::AhrensBeylkin; @@ -21,7 +22,7 @@ AngularQuad angular_from_string(std::string name) { throw std::runtime_error("Unrecognized Angular Quadrature"); } -S2Factory::s2_grid_ptr S2Factory::generate(AngularQuad aq, size_t npts) { +INTEGRATORXX_GENERATOR_LINKAGE S2Factory::s2_grid_ptr S2Factory::generate(AngularQuad aq, size_t npts) { switch(aq) { case AngularQuad::AhrensBeylkin: diff --git a/include/integratorxx/generators/impl/treutler_pruning.hpp b/include/integratorxx/generators/impl/treutler_pruning.hpp index d59a107..881cf42 100644 --- a/include/integratorxx/generators/impl/treutler_pruning.hpp +++ b/include/integratorxx/generators/impl/treutler_pruning.hpp @@ -1,4 +1,5 @@ #pragma once +#include #include #include @@ -16,7 +17,7 @@ auto get_treutler_low_med_sizes() { return std::make_pair(low_sz, med_sz); } -PrunedSphericalGridSpecification treutler_pruning_scheme_impl( +INTEGRATORXX_GENERATOR_LINKAGE PrunedSphericalGridSpecification treutler_pruning_scheme_impl( size_t low_sz, size_t med_sz, AngularQuad angular_quad, UnprunedSphericalGridSpecification unp ) { @@ -39,7 +40,7 @@ PrunedSphericalGridSpecification treutler_pruning_scheme_impl( -PrunedSphericalGridSpecification treutler_pruning_scheme( +INTEGRATORXX_GENERATOR_LINKAGE PrunedSphericalGridSpecification treutler_pruning_scheme( UnprunedSphericalGridSpecification unp ) { size_t low_sz, med_sz; diff --git a/include/integratorxx/generators/impl/unpruned_grid.hpp b/include/integratorxx/generators/impl/unpruned_grid.hpp index 758b1fd..9821aec 100644 --- a/include/integratorxx/generators/impl/unpruned_grid.hpp +++ b/include/integratorxx/generators/impl/unpruned_grid.hpp @@ -1,5 +1,6 @@ #pragma once +#include #include #include @@ -37,7 +38,7 @@ auto generate_unpruned_grid_impl(RadialQuad rq, const RadialTraits& traits, } // Implementation details -SphericalGridFactory::spherical_grid_ptr +INTEGRATORXX_GENERATOR_LINKAGE SphericalGridFactory::spherical_grid_ptr SphericalGridFactory::generate_unpruned_grid( RadialQuad rq, const RadialTraits& traits, AngularQuad aq, AngularSize nang) { diff --git a/include/integratorxx/generators/radial_factory.hpp b/include/integratorxx/generators/radial_factory.hpp index 2d4f0ab..428a3c0 100644 --- a/include/integratorxx/generators/radial_factory.hpp +++ b/include/integratorxx/generators/radial_factory.hpp @@ -76,3 +76,10 @@ struct RadialFactory { }; } + +// Header-only builds carry the implementation with the declarations. This is +// included last so that the declarations above are already visible; the impl +// headers include this one back, which #pragma once makes a no-op. +#ifdef INTEGRATORXX_HEADER_ONLY +#include +#endif diff --git a/include/integratorxx/generators/s2_factory.hpp b/include/integratorxx/generators/s2_factory.hpp index 1e550e4..d34953d 100644 --- a/include/integratorxx/generators/s2_factory.hpp +++ b/include/integratorxx/generators/s2_factory.hpp @@ -39,3 +39,10 @@ struct S2Factory { }; } + +// Header-only builds carry the implementation with the declarations. This is +// included last so that the declarations above are already visible; the impl +// headers include this one back, which #pragma once makes a no-op. +#ifdef INTEGRATORXX_HEADER_ONLY +#include +#endif diff --git a/include/integratorxx/generators/spherical_factory.hpp b/include/integratorxx/generators/spherical_factory.hpp index 4937503..8fc2d4d 100644 --- a/include/integratorxx/generators/spherical_factory.hpp +++ b/include/integratorxx/generators/spherical_factory.hpp @@ -188,3 +188,11 @@ struct SphericalGridFactory { }; } + +// Header-only builds carry the implementation with the declarations. This is +// included last so that the declarations above are already visible; the impl +// headers include this one back, which #pragma once makes a no-op. +#ifdef INTEGRATORXX_HEADER_ONLY +#include +#include +#endif diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index e1a2460..0d32576 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -23,9 +23,6 @@ else() endif() add_library(integratorxx_common_ut quad_matcher.cxx) -if(INTEGRATORXX_HEADER_ONLY) - target_sources(integratorxx_common_ut PRIVATE lib_impl.cxx) -endif() target_link_libraries( integratorxx_common_ut PUBLIC Catch2::Catch2WithMain integratorxx ) add_executable( quadrature_manipulation quadrature_manipulation.cxx ) diff --git a/test/lib_impl.cxx b/test/lib_impl.cxx deleted file mode 100644 index ba83f11..0000000 --- a/test/lib_impl.cxx +++ /dev/null @@ -1,2 +0,0 @@ -#include -