Skip to content

fmtlib adoption extension#3375

Draft
Lestropie wants to merge 9 commits into
fmtfrom
fmt_extension
Draft

fmtlib adoption extension#3375
Lestropie wants to merge 9 commits into
fmtfrom
fmt_extension

Conversation

@Lestropie

Copy link
Copy Markdown
Member

Potential extension of #3339.
Using that branch as the base here to evaluate changes given I've already gone to the effort of manually code-reviewing #3339 in its current form.
Draft for now; deferring to a better time for me to review manually.

Had a go at further reducing redundancies between legacy MRtrix string-handling functions and fmtlib capabilities.

  • Consider re-introduction of MR::str() function, but it just wraps fmt::format("{}", item).
  • Where stream << operators are involved, collapse the whole operation into a single fmt::format() call.
  • Remove unnecessary static_cast<> calls on MR::Helper::Index and MR::Helper::Value.
  • Manually check string outputs for any points of concern (thinking MR::printf() usages, confirm column vector transposition display)

Lestropie and others added 9 commits May 28, 2026 23:01
Delete the str<cfloat>/str<cdouble> specialisations in core/mrtrix.h, which
emitted a bespoke complex representation (re+imi, parens-free, imaginary part
suppressed when zero). All complex str() call sites now route through fmtlib's
<fmt/std.h> formatter ((re+imi) form) via explicit fmt::format("{}", ...):

- core/stats.h: mean/std/std_rv/min/max (refined further in Task 2)
- cmd/mrcalc.cpp: calculator-mode constant stringification

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replace the runtime is_complex flag in MR::Stats with compile-time type
dispatch, so guaranteed-real data never engages fmtlib's complex formatter:

- Stats becomes a class template Stats<T> (T in {default_type, cdouble}); the
  Welford accumulators are typed T, with if constexpr (is_complex<T>) selecting
  the real/imag-separated min/max/std logic vs the scalar form. std_rv and the
  median values vector are real-valued and guarded accordingly. operator() is
  explicitly instantiated for both domains; print_header is now a template.
- mrstats reads header.datatype().is_complex() once and dispatches to a
  templated run_impl<T>() opening Image<T>; Volume_loop and run_volume are
  templated on T.

Adopting fmtlib's (a+bi) complex form left MR::to<cfloat>/to<cdouble> unable to
parse the software's own output (still only read the bare a+bi). Extend both
parsers to also accept the parenthesised form, restoring write/read symmetry
(mrcalc complex constants, complex matrix I/O, the diff-matrix test harness).
Extend the MR::to<>() unit test (to_tests.cpp) with parenthesised inputs and a
round-trip value check. The mrstats complex reference test gains -frac 1e-5 to
accommodate fmt's shortest-round-trip precision (matching its real siblings).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Convert the 13 str(value, precision) sites per per-site decisions, replacing
each with fmt::format. Discard the precision (accept fmt's shortest round-trip)
where it was incidental, and propagate it as {:.Ng} where an external spec or a
fixed display format demands a stable digit count:

- Discard: matrix.h vector .txt writes (reload exactly via to<>); MGH header
  keyvals (TR/flip/TE/TI); DICOM value-string; mrinfo voxel-size description;
  BIDS TotalReadoutTime (default precision).
- Propagate: DICOM time subseconds {:.6g} (DICOM TM allows <=6 fractional
  digits); track-file timestamp {:.{}g} with file_timestamp_precision (20);
  MRView FPS readout {:.4g}.

Converting the generic matrix.h vector write to fmt exposed Eigen::half, which
has no native fmt formatter; add an fmt::formatter<Eigen::half> : ostream_formatter
bridge in half.h (delegates to Eigen's operator<<). [Recorded for the Task 7
nontrivial-class tabulation.]

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The 17 str<T>(expr) coercion sites forced a target type before stringifying.
Replace each with fmt::format("{}", static_cast<T>(expr)): the static_cast
performs the type coercion (for ParsedArgument operands this exercises the
intended operator unsigned int()/operator float() conversion), and fmt's
shortest-round-trip formatting replaces str()'s max_digits10 default.

Stream call sites (5ttedit verbose output, surface/mesh.cpp VTK vertex writes,
tckstats histogram counts) are wrapped in fmt::format rather than emitted bare
into the ostream, so fmt's formatting is retained instead of falling back to
iostream's 6-significant-figure default. Keyval/properties assignments
(mrhistmatch, seeding, tracking, iFOD1/2) take the std::string result directly.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replace the bespoke fmt container/Eigen formatters in core/types.h with fmtlib's
native facilities:

- Delete the custom formatter<std::vector<T>> and formatter<std::array<T,N>>;
  add <fmt/ranges.h>. Container output style changes from "[ x y z ]" to fmt's
  "[1, 2, 3]" (strings quoted).
- The Eigen matrix/array formatter now delegates to Eigen's own ostream operator<<
  (via fmt::streamed), special-casing a column vector by emitting its transpose on
  a single line with a trailing "^T" (transpose done inside the formatter). Since
  Eigen types satisfy fmtlib's is_range, range_format_kind<Eigen...> is specialised
  to range_format::disabled so the dedicated formatter applies unambiguously rather
  than colliding with the generic range formatter. Eigen::Transform and
  Eigen::WithFormat formatters are retained.

Call-site/test rectification for the style change:
- dirstat.cpp: a vector formatted with "{:.6g}" must use fmtlib's per-element range
  spec "{::.6g}" (a whole-range numeric spec now throws at runtime).
- dirrotate/labelvalidate tests parsed command output by field position / spaced
  tokens; updated to the new comma/bracket layout (strip brackets+commas; word-
  boundary match).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
fmt::join(range, delimiter) produces exactly MRtrix's delimiter-only output (no
brackets). Convert all ~40 MR::join call sites across cmd/, core/ and gui/:
- Where join() was a direct argument to an fmt-style sink (fmt::format, CONSOLE/
  INFO/WARN/Exception, Option/Argument descriptions), inline it as fmt::join(...).
- Where a std::string result was required (keyval assignment, '+' concatenation,
  stream insertion), wrap as fmt::format("{}", fmt::join(...)).

Remove the MR::join declarations/definitions (core/mrtrix.h, core/mrtrix.cpp),
including the unused null-terminated char*const* overload. core/enum.h's unrelated
detail::join is left intact. <fmt/ranges.h> (added in Task 6) supplies fmt::join.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Convert the ~30 C-style printf() call sites to fmt::format brace syntax,
translating the format specifiers: %04X->{:04X}, %8 PRIu64->{:8},
%3 PRI_SIZET %%->{:3}%, %.4g->{:.4g}, %5u->{:5}, %3.3f%%->{:3.3f}%,
%c%c->{}{}, %*.*d (width==precision==N)->{:0{}} (zero-pad to N digits).

- progressbar.cpp: ANSI WRAP_*/CLEAR_LINE_CODE literal macros concatenate into
  the fmt format string unchanged (no braces inside them).
- exception.cpp: the per-colour format strings (held in a runtime map) become
  brace templates rendered via fmt::format(fmt::runtime(...), ...).
- dicom/image.cpp: DEBUG(printf(...)) collapses to DEBUG("...{:04X}...", ...)
  since DEBUG already takes an fmt format string.

Remove MR::printf and its -Wformat-security pragma block from core/mrtrix.h.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replace the column-vector auto-transpose in the fmtlib Eigen formatter
with an opt-in `T` spec character: `{:T}` transposes the value and appends
the `^T` suffix, while `{}` defers to Eigen's native ostream layout. The
thirteen call sites whose explicit `.transpose()` was stripped in commit
e177c00 (check_gradient.h, gradient_descent_bb.h, evaluate.h, the two
initialiser*.cpp files) now carry `:T` and so render identically to the
prior auto-transpose behaviour.

Rewrite `MR::to<T>()` on top of `std::from_chars` for arithmetic types,
retaining `std::istringstream` as a per-family fallback whenever the
standard library in use lacks the corresponding from_chars support.
CMake configure-time checks (`MRTRIX_HAVE_FROM_CHARS_INT`,
`MRTRIX_HAVE_FROM_CHARS_FP`) gate the dispatch. The all-input-consumed
invariant is preserved, and `errc::result_out_of_range` now surfaces as
a distinct overflow Exception rather than silently saturating. The bool
and complex specialisations are unchanged; the floating-point keyword
fallback (`nan`/`-nan`/`inf`/`-inf`) is shared between both backends.

Augment the unit tests with: an fmtlib<->MR::to<>() complex round-trip
driven directly off `fmt::format("{}", value)` output; rejection of
parenthesised inputs for real-valued conversions; and exhaustive
integer/float overflow detection. Two existing expectations
(`infinity`/`-infinity`) flip to accept, matching from_chars semantics
per C++17.

Session prompts:
1. > 1. In the custom Eigen fmtlib formatter, rather than auto-detecting
   > that the input is a column vector and automatically printing its
   > transpose and adding the "^T" suffix, instead implement a custom
   > formatting specifier that nominates for the transpose of the input
   > to be displayed. For all printing / macro calls involving Eigen
   > classes that were refactored in a recent commit to remove the
   > explicit call to .transpose(), insert use of the custom formatter.
   > 2. In function MR::to<>(), change the underlying implementation from
   > std::istringstream to C++17 std::from_chars(). Ensure that the
   > check that all content of the input string was utilised in the
   > conversion was used persists. Ensure that the unit tests for that
   > function still pass. Ensure augmentation of these unit tests to
   > prove that for specifically complex-valued outputs, the round
   > brackets produced by fmtlib when writing complex numbers are
   > handled appropriately, whereas for real-valued data the presence of
   > such brackets is not permitted. Augment unit tests to ensure that
   > datatype range overflow is suitably caught.
2. > In the transition to std::from_chars(), it will be necessary to add
   > a test during configuration to make sure that the implementation of
   > that function for the compiler in use supports all required data
   > types; where it does not, the implementation must fall back via
   > precompiler directives to the current implementation.

Generated-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@Lestropie Lestropie self-assigned this May 28, 2026
@Lestropie Lestropie changed the title Fmt extension fmtlib adoption extension May 28, 2026

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

clang-tidy made some suggestions

There were too many comments to post at once. Showing the first 25 out of 33. Check the log or trigger a new build to see more.

Comment thread cpp/cmd/mrstats.cpp

private:
Image<complex_type> &image;
Image<T> &image;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

warning: member 'image' of type 'Image &' is a reference [cppcoreguidelines-avoid-const-or-ref-data-members]

  Image<T> &image;
            ^

Comment thread cpp/cmd/mrstats.cpp

private:
Image<complex_type> &image;
Image<T> &image;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

warning: member 'image' of type 'Image<complex> &' is a reference [cppcoreguidelines-avoid-const-or-ref-data-members]

  Image<T> &image;
            ^

Comment thread cpp/cmd/mrstats.cpp

private:
Image<complex_type> &image;
Image<T> &image;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

warning: member 'image' of type 'Image &' is a reference [cppcoreguidelines-avoid-const-or-ref-data-members]

  Image<T> &image;
            ^

Comment thread cpp/cmd/tckstats.cpp
out << "Length,Sum_weights\n";
for (size_t i = 0; i != histogram.size(); ++i)
out << str(i * step_size) << "," << str(histogram[i]) << "\n";
out << fmt::format("{}", i * step_size) << "," << fmt::format("{}", histogram[i]) << "\n";

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

warning: narrowing conversion from 'size_t' (aka 'unsigned long') to 'float' [bugprone-narrowing-conversions]

        out << fmt::format("{}", i * step_size) << "," << fmt::format("{}", histogram[i]) << "\n";
                                 ^

Comment thread cpp/cmd/tckstats.cpp
out << "Length,Count\n";
for (size_t i = 0; i != histogram.size(); ++i)
out << str(i * step_size) << "," << str<size_t>(histogram[i]) << "\n";
out << fmt::format("{}", i * step_size) << "," << fmt::format("{}", static_cast<size_t>(histogram[i])) << "\n";

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

warning: narrowing conversion from 'size_t' (aka 'unsigned long') to 'float' [bugprone-narrowing-conversions]

        out << fmt::format("{}", i * step_size) << "," << fmt::format("{}", static_cast<size_t>(histogram[i])) << "\n";
                                 ^

Comment thread cpp/gui/dialog/opengl.cpp
root->appendChild(new TreeItem("VSync", format.swapInterval() ? "on" : "off", root));
root->appendChild(
new TreeItem("Multisample anti-aliasing", format.samples() ? str(format.samples()).c_str() : "off", root));
root->appendChild(new TreeItem(

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

warning: initializing non-owner argument of type 'TreeItem *' with a newly created 'gsl::owner<>' [cppcoreguidelines-owning-memory]

  root->appendChild(new TreeItem(
                    ^

Comment thread cpp/gui/dialog/opengl.cpp
root->appendChild(
new TreeItem("Multisample anti-aliasing", format.samples() ? str(format.samples()).c_str() : "off", root));
root->appendChild(new TreeItem(
"Multisample anti-aliasing", format.samples() ? fmt::format("{}", format.samples()).c_str() : "off", root));

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

warning: implicit conversion 'int' -> bool [readability-implicit-bool-conversion]

Suggested change
"Multisample anti-aliasing", format.samples() ? fmt::format("{}", format.samples()).c_str() : "off", root));
"Multisample anti-aliasing", format.samples() != 0 ? fmt::format("{}", format.samples()).c_str() : "off", root));

Comment thread cpp/gui/dialog/opengl.cpp
GLint max_2d_texture_size;
gl::GetIntegerv(gl::MAX_TEXTURE_SIZE, &max_2d_texture_size);
root->appendChild(new TreeItem("Maximum 2D texture size", str(max_2d_texture_size), root));
root->appendChild(new TreeItem("Maximum 2D texture size", fmt::format("{}", max_2d_texture_size), root));

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

warning: initializing non-owner argument of type 'TreeItem *' with a newly created 'gsl::owner<>' [cppcoreguidelines-owning-memory]

  root->appendChild(new TreeItem("Maximum 2D texture size", fmt::format("{}", max_2d_texture_size), root));
                    ^

Comment thread cpp/gui/dialog/opengl.cpp
GLint max_3D_texture_size;
gl::GetIntegerv(gl::MAX_3D_TEXTURE_SIZE, &max_3D_texture_size);
root->appendChild(new TreeItem("Maximum 3D texture size", str(max_3D_texture_size), root));
root->appendChild(new TreeItem("Maximum 3D texture size", fmt::format("{}", max_3D_texture_size), root));

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

warning: initializing non-owner argument of type 'TreeItem *' with a newly created 'gsl::owner<>' [cppcoreguidelines-owning-memory]

  root->appendChild(new TreeItem("Maximum 3D texture size", fmt::format("{}", max_3D_texture_size), root));
                    ^

}
if (index.column() == 0 && role == Qt::DisplayRole)
return qstr(str(index.row()));
return qstr(fmt::format("{}", index.row()));

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

warning: repeated branch in conditional chain [bugprone-branch-clone]

    return qstr(fmt::format("{}", index.row()));
    ^
Additional context

cpp/gui/mrview/tool/connectome/node_list.cpp:45: end of the original

    return qstr(fmt::format("{}", index.row()));
          ^

cpp/gui/mrview/tool/connectome/node_list.cpp:49: clone 1 starts here

    return qstr(connectome.nodes[index.row()].get_name());
    ^

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant