Skip to content

Fix #369: drop noexcept from allocating paths - #436

Merged
petlenz merged 2 commits into
mainfrom
fix-369-noexcept-allocating
Sep 16, 2026
Merged

petlenz merged 2 commits into
mainfrom
fix-369-noexcept-allocating

Conversation

@petlenz

@petlenz petlenz commented Sep 15, 2026

Copy link
Copy Markdown
Member

Closes #369.

noexcept on paths that can allocate turned std::bad_alloc into std::terminate. Audited every noexcept in include/ and src/ (287 sites).

Removed (can allocate or throw):

  • expression::operator==, !=, <, like_term_of (+ n_ary_tree, tensor_scalar_mul overrides), equals_same_type / less_than_same_type (+ visitor_base overrides): all compute child hashes lazily.
  • update_hash_value overrides that hash children: n_ary_tree (also allocates a vector), unary_op, binary_op, ternary_op and their update_hash functors, tensor_scalar_mul, tensor_pow, permute_indices_wrapper, inner_product_wrapper, outer_product_wrapper, tensor_eigenprojection, tensor_eigenvector, tensor_isotropic_function, tensor_inner_product_to_scalar, tensor_to_scalar_divided_difference, tensor_to_scalar_eigenvalue, tensor_to_scalar_scalar_wrapper. (Signature lines only; Fix #372: reset m_hash_value before recomputing in every hash override #431 edits some of these bodies.)
  • n_ary_tree: copy and forwarding ctors. n_ary_vector: default ctor (reserve), copy and forwarding ctors, push_back, reserve, insert_hash.
  • symbol_base(name, ...) (copies the name), get_scalar_zero/one (static init allocates), symbol_table::has (builds a std::string).
  • Comparators scalar_expr_less, scalar_pretty_printer; printer_base::print_sequence, latex_printer_base::print_unary; structural_propagation::preserve_unary (copies a space); tensor_data_make_imp::evaluate_imp (make_unique); the tensor-with-scalar and tensor-with-t2s mul dispatch functions (build expressions).

Moves that copied — now genuine moves, noexcept kept: tensor_add, tensor_mul, simple_outer_product move the space instead of copying it (dropping noexcept there trips clang-tidy performance-noexcept-move-constructor); permute_indices_wrapper moved via static_cast<base> (a copy), now static_cast<base &&>; scalar move-assignment moves the name.

Kept: move ctors/assignments that only move members, getters, is_same, set_coeff (shared_ptr assignment), set_space/clear_space, scalar_constant::update_hash_value (hashes a scalar_number, no children), scalar_number::abs, printer_base::begin/end and tensor_data::print (stream insertion), fixed-size tmech evaluate_imps, index arithmetic helpers, conditional noexcept(noexcept(tag_invoke(...))) CPOs, and the never-instantiated static_assert evaluator fallbacks.

TestsCoreBugFix.AllocatingPathsAreNotNoexcept:

  • static_asserts that ==, <, like_term_of, n_ary_vector::push_back and get_scalar_zero() are not noexcept; that scalar_add, tensor_add, tensor_to_scalar_mul, permute_indices_wrapper stay nothrow-move-constructible.
  • Runtime: moving a tensor_add keeps its Symmetric space.

Negative control: compiling the test against main's sources fails on the five "not noexcept" asserts; removing the space transfer from tensor_add's move ctor fails the runtime check.

Full suite 2347/2347 (gcc-14 Debug); clang-tidy-18 clean on the changed .cpp files; clang-format-18 clean.


Review fix (eb5ce92). The forwarding move ctors n_ary_tree(n_ary_tree&&, Args&&...) and n_ary_vector(n_ary_vector&&, Args&&...) kept unconditional noexcept while their copy twins lost it — same over-promise shape, since Args is forwarded into base_t. Both are now noexcept(std::is_nothrow_constructible_v<base_t, Args...>). To keep that condition true for the tensor nodes, tensor_expression(dim, rank) and tensor_expression(tensor_expression&&, dim, rank) are marked noexcept — they only store two size_t and default-init the remaining members. Plain move ctors stay unconditionally noexcept; added static_asserts for scalar_mul, tensor_mul, tensor_to_scalar_add and both expression_holder instantiations, so container nothrow-move is pinned.

Two observations left as-is (out of scope): scalar_expr_less in numsim_cas_type_traits.h is dead code — expr_ordered_map is std::map<T, T> with the default std::less, and the struct has no other reference; and scalar::operator=(scalar&&) has no callers (scalar also pulls in using base_t::operator=).

Full suite 2347/2347 after the review fix; clang-tidy-18 and clang-format-18 clean.

bad_alloc on these paths called std::terminate instead of propagating:
- expression ==, !=, <, like_term_of and the equals/less_than_same_type
  hooks (they compute hashes lazily, which allocates)
- update_hash_value overrides that hash children (n_ary_tree's also
  allocates a vector)
- n_ary_tree/n_ary_vector copy and forwarding ctors, n_ary_vector
  default ctor, push_back, reserve, insert_hash
- symbol_base(name, ...) (copies the name), get_scalar_zero/one (static
  init allocates), symbol_table::has (builds a std::string)
- scalar_expr_less and scalar_pretty_printer comparators, printer
  print_sequence, latex print_unary, preserve_unary (copies a space),
  tensor_data_make_imp::evaluate_imp (make_unique), tensor-with-scalar
  and tensor-with-t2s mul dispatches (build expressions)

Moves that copied now move and keep noexcept: tensor_add, tensor_mul and
simple_outer_product transfer the space; permute_indices_wrapper moves
its base instead of copying it; scalar move-assignment moves the name.

Signed-off-by: petlenz <peterlenz89.pl@gmail.com>
n_ary_tree/n_ary_vector's (self&&, Args&&...) ctors promised noexcept
while forwarding Args into base_t. They are now
noexcept(is_nothrow_constructible_v<base_t, Args...>); the sized
tensor_expression ctors they call are marked noexcept since they only
store two size_t and default-init the rest, so node types keep their
nothrow move. static_asserts cover scalar_mul, tensor_mul,
tensor_to_scalar_add and both expression_holder instantiations.

Signed-off-by: petlenz <peterlenz89.pl@gmail.com>
@petlenz
petlenz merged commit 6f7d4be into main Sep 16, 2026
27 checks passed
@petlenz
petlenz deleted the fix-369-noexcept-allocating branch September 16, 2026 22:21
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

n_ary_tree: noexcept on allocating paths (copy ctor, update_hash_value) turns bad_alloc into std::terminate

1 participant