Summary
The three emitter classes and both targets build their output via std::ostringstream << "..." << x << ", " patterns. Examples:
include/numsim_codegen/recipe.h:287-336 (render_compute_function)
include/numsim_codegen/code_emit/codegen_context.h (render_statements)
include/numsim_codegen/code_emit/scalar_code_emit.h:60-79 (constant emission)
include/numsim_codegen/targets/moose_material.h:64-223 (header + source emission)
Each << chain has to be read top-to-bottom to figure out what the output looks like. The code is fine but verbose.
The project is C++23 (CMakeLists.txt:9), so std::format is available with zero new deps. Many of these chains collapse to one-liners.
Proposed shape
Before:
os << " tmech::adaptor<double const, " << i.dim << " , " << i.rank
<< " , tmech::full<" << i.dim << " >> " << i.name
<< " _ad(_" << i.name << " [_qp].dataPointer());\n " ;
After:
os << std::format(
" tmech::adaptor<double const, {0}, {1}, tmech::full<{0}>> {2}_ad(_{2}[_qp].dataPointer());\n " ,
i.dim, i.rank, i.name);
Indexed args ({0}, {2}) let us reference dim and name multiple times without repeating them at the call site. The intent ("the output is this template , filled with these values ") becomes visible at a glance.
Why this is worth doing
Readability — every emit site reads like the output it produces, not like a stream of <<s.
Type safety — std::format rejects type mismatches at compile time where ostringstream would silently convert.
Locale-independence — ostringstream uses the global C++ locale for numeric formatting; std::format is locale-independent by default. A user with a non-C locale could currently get "1,5" instead of "1.5" in generated code (latent bug — also addressed by Negative-path test coverage: validate(), History throw, all tensor stubs, duplicate-add #11 / Code review follow-up: Med & Low findings (umbrella) #14 's coverage gap).
No new dependency — already in libstdc++ ≥ 13 and libc++ ≥ 17, both of which the CI matrix uses (gcc-14, clang-18 per .github/workflows/build.yml).
Tradeoffs
Pro: zero new deps, smaller / safer / clearer call sites.
Pro: plays well with the inja templating issue — both ultimately reduce the "C++ as string concatenation" pattern.
Con: mechanical rewrite touching ~30 emit sites. Pure churn unless bundled with another change.
Con: std::format's compile-time format-string validation is strict; some << patterns (e.g. mixed-type expressions) need manual restructuring.
Scope
Two reasonable paths:
Standalone PR — rewrite all emit sites in one pass. Pure churn, no behavior change. Pairs well with strict CI warnings landing first.
Bundle with Extract reusable C++ FEM-framework scaffolding (CxxFrameworkTarget + TypeMap + TmechBoundaryAdaptor) #19 — extract CxxFrameworkTarget and rewrite emit sites in the new scaffolding only; legacy MOOSE code converts when it migrates. Cleaner, less diff noise.
Recommend path 2 unless #19 stalls.
Refs
Summary
The three emitter classes and both targets build their output via
std::ostringstream << "..." << x << ", "patterns. Examples:include/numsim_codegen/recipe.h:287-336(render_compute_function)include/numsim_codegen/code_emit/codegen_context.h(render_statements)include/numsim_codegen/code_emit/scalar_code_emit.h:60-79(constant emission)include/numsim_codegen/targets/moose_material.h:64-223(header + source emission)Each
<<chain has to be read top-to-bottom to figure out what the output looks like. The code is fine but verbose.The project is C++23 (
CMakeLists.txt:9), sostd::formatis available with zero new deps. Many of these chains collapse to one-liners.Proposed shape
Before:
After:
os << std::format( "tmech::adaptor<double const, {0}, {1}, tmech::full<{0}>> {2}_ad(_{2}[_qp].dataPointer());\n", i.dim, i.rank, i.name);Indexed args (
{0},{2}) let us referencedimandnamemultiple times without repeating them at the call site. The intent ("the output is this template, filled with these values") becomes visible at a glance.Why this is worth doing
<<s.std::formatrejects type mismatches at compile time whereostringstreamwould silently convert.ostringstreamuses the global C++ locale for numeric formatting;std::formatis locale-independent by default. A user with a non-C locale could currently get"1,5"instead of"1.5"in generated code (latent bug — also addressed by Negative-path test coverage: validate(), History throw, all tensor stubs, duplicate-add #11 / Code review follow-up: Med & Low findings (umbrella) #14's coverage gap)..github/workflows/build.yml).Tradeoffs
std::format's compile-time format-string validation is strict; some<<patterns (e.g. mixed-type expressions) need manual restructuring.Scope
Two reasonable paths:
CxxFrameworkTargetand rewrite emit sites in the new scaffolding only; legacy MOOSE code converts when it migrates. Cleaner, less diff noise.Recommend path 2 unless #19 stalls.
Refs
CxxFrameworkTargetextraction) or the inja templating issue