// Copyright (C) 2026 Kiyotsugu Arai // SPDX-License-Identifier: LGPL-3.0-or-later // expr_templates.hpp // Expression templates — lazy evaluation for vectors and matrices // // Eliminates temporary objects from chained operations like v1 + v2 + v3, computing // every element in a single loop. // // Usage: // Vector v = v1 + 2.0 * v2 - v3 / 3.0; // no intermediate Vector created // Matrix m = m1 + m2 - 3.0 * m3; // no intermediate Matrix created // // Caution: holding an expression with auto risks dangling references. // auto expr = v1 + v2; // expr holds references to v1, v2 — mind their lifetime // Vector v = v1 + v2; // safe (evaluated immediately) #ifndef SANGI_EXPR_TEMPLATES_HPP #define SANGI_EXPR_TEMPLATES_HPP #include #include #include #include #include #include #include namespace sangi { // Forward declarations of Matrix / Vector (so expr_operand_store can specialize within expression templates) template class Matrix; template class Vector; // Base marker for expression-template types (used to constrain scalar operators) struct ExprBase {}; // ================================================================ // Operand-storage selection (preserves value category) // ================================================================ // Because Matrix * Matrix is evaluated eagerly (returning a Matrix), prvalue // Matrices may appear as operands inside an ET expression: // Matrix m = A*B + C*D + E; // A*B and C*D are prvalue Matrices // // The previous implementation accepted operator+(const MatExpr&, const MatExpr&) // and MatBinOp stored const L&, so prvalue Matrix temporaries ended up as // dangling references inside the ET chain. // (Depending on heap fragmentation, correct values sometimes came out, and we // observed nondeterministic n>=20 residuals jumping to the order of ||Q||.) // // Fix: use forwarding references at operator boundaries to preserve value category. // Additionally, wrap lvalues in std::reference_wrapper for storage // (raw const T& members triggered UB under MSVC /O2 inlining). // - lvalue input → std::reference_wrapper (zero copy, value type) // - rvalue input → T (owned by the ET node) template using expr_store_t = std::conditional_t, std::reference_wrapper>, std::remove_cvref_t>; // Extract the stored operand as a reference (used inside operator() and packet()) // For reference_wrapper, call .get(); otherwise return the value as-is template const T& expr_unwrap(const std::reference_wrapper& x) noexcept { return x.get(); } template requires (std::derived_from) const T& expr_unwrap(const T& x) noexcept { return x; } // Factory that converts an operand into its storage type template auto make_expr_store(T&& x) { if constexpr (std::is_lvalue_reference_v) { return std::cref(x); } else { return std::remove_cvref_t(std::forward(x)); } } // Obtain the underlying expression type from the store type (T or std::reference_wrapper) template struct expr_underlying { using type = std::remove_cvref_t; }; template struct expr_underlying> { using type = U; }; template struct expr_underlying> { using type = U; }; template using expr_underlying_t = typename expr_underlying>::type; template concept ExprLike = std::derived_from, ExprBase>; // ================================================================ // Vector expression templates // ================================================================ template struct VecExpr : ExprBase { const Derived& derived() const noexcept { return static_cast(*this); } }; // Concept for VecExpr-derived types (checks whether VecExpr is a base class) template concept VecExprLike = std::derived_from, ExprBase> && requires(std::remove_cvref_t const& v) { v.size(); v[0]; }; // Detect matrix-like types (those exposing rows()/cols()) — prevents Matrix/SparseMatrix // from being passed as VecScale's S and being interpreted as value_type * Matrix. namespace detail { template struct has_matrix_shape : std::false_type {}; template struct has_matrix_shape().rows()), decltype(std::declval().cols())>> : std::true_type {}; } // Constraint on S for VecScale: not derived from ExprBase and not matrix-like (i.e., scalar-like) template concept ScalarLike = !std::derived_from, ExprBase> && !detail::has_matrix_shape>::value; // Unary minus template struct VecNeg : VecExpr> { using Expr = expr_underlying_t; using value_type = typename Expr::value_type; EStore expr_; template explicit VecNeg(E&& e) : expr_(make_expr_store(std::forward(e))) {} value_type operator[](std::size_t i) const { return -expr_unwrap(expr_)[i]; } std::size_t size() const { return expr_unwrap(expr_).size(); } auto packet(std::size_t i) const { return PacketTraits::negate(expr_unwrap(expr_).packet(i)); } }; // Scalar multiplication template struct VecScale : VecExpr> { using Expr = expr_underlying_t; using value_type = typename Expr::value_type; EStore expr_; S scalar_; template VecScale(E&& e, S s) : expr_(make_expr_store(std::forward(e))), scalar_(s) {} value_type operator[](std::size_t i) const { return static_cast(expr_unwrap(expr_)[i] * scalar_); } std::size_t size() const { return expr_unwrap(expr_).size(); } auto packet(std::size_t i) const { using PT = PacketTraits; return PT::mul(expr_unwrap(expr_).packet(i), PT::set1(static_cast(scalar_))); } }; // Scalar division (converted to reciprocal multiplication; div is ~20 cycles, mul is ~4 cycles) template struct VecScaleDiv : VecExpr> { using Expr = expr_underlying_t; using value_type = typename Expr::value_type; EStore expr_; value_type inv_; // 1/scalar precomputed template VecScaleDiv(E&& e, S s) : expr_(make_expr_store(std::forward(e))), inv_(value_type(1) / static_cast(s)) {} value_type operator[](std::size_t i) const { return static_cast(expr_unwrap(expr_)[i] * inv_); } std::size_t size() const { return expr_unwrap(expr_).size(); } auto packet(std::size_t i) const { using PT = PacketTraits; return PT::mul(expr_unwrap(expr_).packet(i), PT::set1(inv_)); } }; // Helper trait that detects VecScale (used for FMA fusion) template struct is_vec_scale : std::false_type {}; template struct is_vec_scale> : std::true_type {}; // Binary-operation node (addition / subtraction) // Detects Scale+Add/Sub and fuses them into FMA: mul+add (8c) → fmadd (4c) template struct VecBinOp : VecExpr> { using LExpr = expr_underlying_t; using RExpr = expr_underlying_t; using value_type = typename LExpr::value_type; LStore lhs_; RStore rhs_; template VecBinOp(L&& l, R&& r) : lhs_(make_expr_store(std::forward(l))), rhs_(make_expr_store(std::forward(r))) { if (expr_unwrap(lhs_).size() != expr_unwrap(rhs_).size()) throw DimensionError("Vector expression: size mismatch"); } value_type operator[](std::size_t i) const { return Op{}(expr_unwrap(lhs_)[i], expr_unwrap(rhs_)[i]); } std::size_t size() const { return expr_unwrap(lhs_).size(); } // SIMD packet evaluation — fuse Scale+Add/Sub into FMA auto packet(std::size_t i) const { using PT = PacketTraits; const auto& lhs = expr_unwrap(lhs_); const auto& rhs = expr_unwrap(rhs_); if constexpr (std::is_same_v> && is_vec_scale::value) { // s*a + b → fmadd(a, s, b) return PT::fmadd(expr_unwrap(lhs.expr_).packet(i), PT::set1(static_cast(lhs.scalar_)), rhs.packet(i)); } else if constexpr (std::is_same_v> && is_vec_scale::value) { // a + s*b → fmadd(b, s, a) return PT::fmadd(expr_unwrap(rhs.expr_).packet(i), PT::set1(static_cast(rhs.scalar_)), lhs.packet(i)); } else if constexpr (std::is_same_v> && is_vec_scale::value) { // a - s*b → fmadd(b, -s, a) return PT::fmadd(expr_unwrap(rhs.expr_).packet(i), PT::negate(PT::set1(static_cast(rhs.scalar_))), lhs.packet(i)); } else { return PacketOp::apply(lhs.packet(i), rhs.packet(i)); } } }; // ---- Vector operators (forwarding references preserve value category) ---- template requires (VecExprLike && VecExprLike) auto operator+(L&& a, R&& b) { using LS = expr_store_t; using RS = expr_store_t; return VecBinOp>(std::forward(a), std::forward(b)); } template requires (VecExprLike && VecExprLike) auto operator-(L&& a, R&& b) { using LS = expr_store_t; using RS = expr_store_t; return VecBinOp>(std::forward(a), std::forward(b)); } template requires VecExprLike auto operator-(E&& e) { using ES = expr_store_t; return VecNeg(std::forward(e)); } /// Element-wise subtraction scalar - vector (s - v[i]). /// No dedicated ET node exists yet, so the result is materialized into a Vector and returned. template requires (ScalarLike && VecExprLike) auto operator-(const S& s, const E& e) { using T = std::remove_cvref_t; Vector r(e.size()); for (std::size_t i = 0; i < e.size(); ++i) r[i] = T(s) - e[i]; return r; } template requires (VecExprLike && ScalarLike) auto operator*(E&& e, const S& s) { using ES = expr_store_t; return VecScale(std::forward(e), s); } template requires (VecExprLike && ScalarLike) auto operator*(const S& s, E&& e) { using ES = expr_store_t; return VecScale(std::forward(e), s); } template requires (VecExprLike && ScalarLike) auto operator/(E&& e, const S& s) { if (s == S{0}) throw std::invalid_argument("Division by zero"); using ES = expr_store_t; return VecScaleDiv(std::forward(e), s); } // ================================================================ // Matrix expression templates (element-wise operations only) // Matrix multiplication (matrix * matrix, matrix * vector) is not lazy // ================================================================ template struct MatExpr : ExprBase { const Derived& derived() const noexcept { return static_cast(*this); } }; // Concept for MatExpr-derived types template concept MatExprLike = std::derived_from, ExprBase> && requires(std::remove_cvref_t const& m) { m.rows(); m.cols(); m(0, 0); }; // Binary-operation node (addition / subtraction) template struct MatBinOp : MatExpr> { using LExpr = expr_underlying_t; using RExpr = expr_underlying_t; using value_type = typename LExpr::value_type; LStore lhs_; RStore rhs_; template MatBinOp(L&& l, R&& r) : lhs_(make_expr_store(std::forward(l))), rhs_(make_expr_store(std::forward(r))) { const auto& lhs = expr_unwrap(lhs_); const auto& rhs = expr_unwrap(rhs_); if (lhs.rows() != rhs.rows() || lhs.cols() != rhs.cols()) throw DimensionError("Matrix expression: dimension mismatch"); } value_type operator()(std::size_t i, std::size_t j) const { return Op{}(expr_unwrap(lhs_)(i, j), expr_unwrap(rhs_)(i, j)); } std::size_t rows() const { return expr_unwrap(lhs_).rows(); } std::size_t cols() const { return expr_unwrap(lhs_).cols(); } // SIMD packet evaluation (linear index) auto packet(std::size_t idx) const { return PacketOp::apply( expr_unwrap(lhs_).packet(idx), expr_unwrap(rhs_).packet(idx)); } }; // Unary minus template struct MatNeg : MatExpr> { using Expr = expr_underlying_t; using value_type = typename Expr::value_type; EStore expr_; template explicit MatNeg(E&& e) : expr_(make_expr_store(std::forward(e))) {} value_type operator()(std::size_t i, std::size_t j) const { return -expr_unwrap(expr_)(i, j); } std::size_t rows() const { return expr_unwrap(expr_).rows(); } std::size_t cols() const { return expr_unwrap(expr_).cols(); } auto packet(std::size_t idx) const { return PacketTraits::negate(expr_unwrap(expr_).packet(idx)); } }; // Scalar multiplication template struct MatScale : MatExpr> { using Expr = expr_underlying_t; using value_type = typename Expr::value_type; EStore expr_; S scalar_; template MatScale(E&& e, S s) : expr_(make_expr_store(std::forward(e))), scalar_(s) {} value_type operator()(std::size_t i, std::size_t j) const { return static_cast(expr_unwrap(expr_)(i, j) * scalar_); } std::size_t rows() const { return expr_unwrap(expr_).rows(); } std::size_t cols() const { return expr_unwrap(expr_).cols(); } auto packet(std::size_t idx) const { using PT = PacketTraits; return PT::mul(expr_unwrap(expr_).packet(idx), PT::set1(static_cast(scalar_))); } }; // Scalar division (converted to reciprocal multiplication) template struct MatScaleDiv : MatExpr> { using Expr = expr_underlying_t; using value_type = typename Expr::value_type; EStore expr_; value_type inv_; template MatScaleDiv(E&& e, S s) : expr_(make_expr_store(std::forward(e))), inv_(value_type(1) / static_cast(s)) {} value_type operator()(std::size_t i, std::size_t j) const { return static_cast(expr_unwrap(expr_)(i, j) * inv_); } std::size_t rows() const { return expr_unwrap(expr_).rows(); } std::size_t cols() const { return expr_unwrap(expr_).cols(); } auto packet(std::size_t idx) const { using PT = PacketTraits; return PT::mul(expr_unwrap(expr_).packet(idx), PT::set1(inv_)); } }; // ---- Matrix operators (element-wise, forwarding references preserve value category) ---- template requires (MatExprLike && MatExprLike) auto operator+(L&& a, R&& b) { using LS = expr_store_t; using RS = expr_store_t; return MatBinOp>(std::forward(a), std::forward(b)); } template requires (MatExprLike && MatExprLike) auto operator-(L&& a, R&& b) { using LS = expr_store_t; using RS = expr_store_t; return MatBinOp>(std::forward(a), std::forward(b)); } template requires MatExprLike auto operator-(E&& e) { using ES = expr_store_t; return MatNeg(std::forward(e)); } template requires (MatExprLike && ScalarLike) auto operator*(E&& e, const S& s) { using ES = expr_store_t; return MatScale(std::forward(e), s); } template requires (MatExprLike && ScalarLike) auto operator*(const S& s, E&& e) { using ES = expr_store_t; return MatScale(std::forward(e), s); } template requires (MatExprLike && !std::derived_from, ExprBase>) auto operator/(E&& e, const S& s) { if (s == S{0}) throw std::invalid_argument("Division by zero"); using ES = expr_store_t; return MatScaleDiv(std::forward(e), s); } } // namespace sangi #endif // SANGI_EXPR_TEMPLATES_HPP