// Copyright (C) 2026 Kiyotsugu Arai // SPDX-License-Identifier: LGPL-3.0-or-later // Tensor.hpp // Tensor class of arbitrary rank // // Features: // - Dynamic rank and dynamic shape // - Stride-based view semantics (transpose/reshape avoid copying data) // - Data sharing via shared_ptr (views safely reference the original storage) // - Conversion to and from Matrix and Vector // - Type constraints via C++23 concepts #ifndef SANGI_TENSOR_HPP #define SANGI_TENSOR_HPP #include #include #include #include #include #include #include #include #include #include #include #include #include #include namespace sangi { // Forward declarations template class Matrix; template class Vector; // ================================================================ // Requirements on the element type of Tensor // ================================================================ template concept TensorScalar = requires(T a, T b) { { a + b } -> std::convertible_to; { a - b } -> std::convertible_to; { a * b } -> std::convertible_to; { -a } -> std::convertible_to; { T(0) }; }; // ================================================================ // Stride computation utilities (detail) // ================================================================ namespace detail { // C-order (row-major) stride computation inline std::vector computeRowMajorStrides(const std::vector& shape) { if (shape.empty()) return {}; std::vector strides(shape.size()); strides.back() = 1; for (size_t i = shape.size() - 1; i > 0; --i) { strides[i - 1] = strides[i] * shape[i]; } return strides; } // Compute the total element count from shape inline size_t computeTotalSize(const std::vector& shape) { if (shape.empty()) return 1; // a rank-0 scalar has exactly 1 element return std::accumulate(shape.begin(), shape.end(), size_t(1), std::multiplies()); } // Determine whether the strides are contiguous (C-order) inline bool isContiguousStrides(const std::vector& shape, const std::vector& strides) { if (shape.empty()) return true; auto expected = computeRowMajorStrides(shape); return strides == expected; } } // namespace detail // ================================================================ // Tensor class // ================================================================ template class Tensor { public: using value_type = T; using size_type = std::size_t; using reference = T&; using const_reference = const T&; using pointer = T*; using const_pointer = const T*; // ============================================================ // Constructors // ============================================================ // Default: rank-0 scalar (value = T(0)) Tensor() : data_(std::make_shared>(1, T(0))) , shape_() , strides_() , offset_(0) { } // With explicit shape (zero-initialized) explicit Tensor(std::vector shape) : data_(std::make_shared>(detail::computeTotalSize(shape), T(0))) , shape_(std::move(shape)) , strides_(detail::computeRowMajorStrides(shape_)) , offset_(0) { } // With explicit shape (initializer_list overload, for overload resolution) explicit Tensor(std::initializer_list shape) : Tensor(std::vector(shape)) { } // shape + initial value Tensor(std::vector shape, const T& value) : data_(std::make_shared>(detail::computeTotalSize(shape), value)) , shape_(std::move(shape)) , strides_(detail::computeRowMajorStrides(shape_)) , offset_(0) { } // shape + initial value (initializer_list overload) Tensor(std::initializer_list shape, const T& value) : Tensor(std::vector(shape), value) { } // shape + flat data Tensor(std::vector shape, std::vector data) : shape_(std::move(shape)) , strides_(detail::computeRowMajorStrides(shape_)) , offset_(0) { size_t expected = detail::computeTotalSize(shape_); if (data.size() != expected) { throw DimensionError("Tensor: data size (" + std::to_string(data.size()) + ") does not match shape (expected " + std::to_string(expected) + ")"); } data_ = std::make_shared>(std::move(data)); } // shape + flat data (initializer_list overload) Tensor(std::initializer_list shape, std::vector data) : Tensor(std::vector(shape), std::move(data)) { } // Scalar (rank-0) explicit Tensor(const T& scalar) : data_(std::make_shared>(1, scalar)) , shape_() , strides_() , offset_(0) { } // Conversion from Matrix (rank-2) explicit Tensor(const Matrix& matrix) : shape_({ matrix.rows(), matrix.cols() }) , strides_(detail::computeRowMajorStrides(shape_)) , offset_(0) { size_t total = matrix.rows() * matrix.cols(); data_ = std::make_shared>(total); for (size_t i = 0; i < matrix.rows(); ++i) { for (size_t j = 0; j < matrix.cols(); ++j) { (*data_)[i * matrix.cols() + j] = matrix(i, j); } } } // Conversion from Vector (rank-1) explicit Tensor(const Vector& vec) : shape_({ vec.size() }) , strides_({ 1 }) , offset_(0) { data_ = std::make_shared>(vec.size()); for (size_t i = 0; i < vec.size(); ++i) { (*data_)[i] = vec[i]; } } // Copy/move Tensor(const Tensor& other) = default; Tensor(Tensor&& other) noexcept = default; Tensor& operator=(const Tensor& other) = default; Tensor& operator=(Tensor&& other) noexcept = default; ~Tensor() = default; // ============================================================ // Element access // ============================================================ // variadic template operator() — runtime rank check template requires (std::convertible_to && ...) reference operator()(Indices... indices) { constexpr size_t N = sizeof...(Indices); if (N != rank()) { throw DimensionError("Tensor::operator(): expected " + std::to_string(rank()) + " indices, got " + std::to_string(N)); } size_t idx_array[] = { static_cast(indices)... }; return (*data_)[computeFlatIndex(std::span(idx_array, N))]; } template requires (std::convertible_to && ...) const_reference operator()(Indices... indices) const { constexpr size_t N = sizeof...(Indices); if (N != rank()) { throw DimensionError("Tensor::operator(): expected " + std::to_string(rank()) + " indices, got " + std::to_string(N)); } size_t idx_array[] = { static_cast(indices)... }; return (*data_)[computeFlatIndex(std::span(idx_array, N))]; } // Runtime indexed access (with bounds checking) reference at(std::span indices) { checkIndices(indices); return (*data_)[computeFlatIndex(indices)]; } const_reference at(std::span indices) const { checkIndices(indices); return (*data_)[computeFlatIndex(indices)]; } // initializer_list overload reference at(std::initializer_list indices) { return at(std::span(indices.begin(), indices.size())); } const_reference at(std::initializer_list indices) const { return at(std::span(indices.begin(), indices.size())); } // Flat index access (logical order based on strides) reference flat(size_t index) { if (index >= size()) { throw IndexError("Tensor::flat: index " + std::to_string(index) + " out of range (size = " + std::to_string(size()) + ")"); } if (isContiguous()) { return (*data_)[offset_ + index]; } // Non-contiguous case: convert the logical index into multi-dimensional indices auto indices = unflattenIndex(index); return (*data_)[computeFlatIndex(std::span(indices))]; } const_reference flat(size_t index) const { if (index >= size()) { throw IndexError("Tensor::flat: index " + std::to_string(index) + " out of range (size = " + std::to_string(size()) + ")"); } if (isContiguous()) { return (*data_)[offset_ + index]; } auto indices = unflattenIndex(index); return (*data_)[computeFlatIndex(std::span(indices))]; } // ============================================================ // Properties // ============================================================ // Rank (number of dimensions) size_t rank() const noexcept { return shape_.size(); } // Shape const std::vector& shape() const noexcept { return shape_; } // Size along a specific axis size_t shape(size_t axis) const { if (axis >= rank()) { throw IndexError("Tensor::shape: axis " + std::to_string(axis) + " out of range (rank = " + std::to_string(rank()) + ")"); } return shape_[axis]; } // Strides const std::vector& strides() const noexcept { return strides_; } // Total element count size_t size() const noexcept { return detail::computeTotalSize(shape_); } // Whether empty (element count is 0) bool empty() const noexcept { for (auto d : shape_) { if (d == 0) return true; } return false; } // Whether the memory is contiguous bool isContiguous() const noexcept { return detail::isContiguousStrides(shape_, strides_); } // Whether this is a rank-0 scalar bool isScalar() const noexcept { return shape_.empty(); } // ============================================================ // Data access // ============================================================ pointer data() noexcept { return data_->data() + offset_; } const_pointer data() const noexcept { return data_->data() + offset_; } // ============================================================ // Modification // ============================================================ // Fill every element with the specified value void fill(const T& value) { ensureOwnership(); for (size_t i = 0; i < size(); ++i) { flat(i) = value; } } // Zero out every element void zero() { fill(T(0)); } // ============================================================ // Shape changes // ============================================================ // reshape: change the shape if the element count matches // Contiguous: view (data sharing); otherwise copy [[nodiscard]] Tensor reshape(std::vector newShape) const { size_t newSize = detail::computeTotalSize(newShape); if (newSize != size()) { throw DimensionError("Tensor::reshape: total size mismatch (current " + std::to_string(size()) + ", requested " + std::to_string(newSize) + ")"); } if (isContiguous()) { // View: shared data Tensor result; result.data_ = data_; result.shape_ = std::move(newShape); result.strides_ = detail::computeRowMajorStrides(result.shape_); result.offset_ = offset_; return result; } else { // Non-contiguous: copy into the new shape Tensor result(newShape); for (size_t i = 0; i < size(); ++i) { result.flat(i) = flat(i); } return result; } } // transpose: axis permutation (view: no data copy) [[nodiscard]] Tensor transpose(std::vector perm) const { if (perm.size() != rank()) { throw DimensionError("Tensor::transpose: permutation size (" + std::to_string(perm.size()) + ") does not match rank (" + std::to_string(rank()) + ")"); } // Validate that perm is a permutation of 0..rank-1 std::vector seen(rank(), false); for (size_t p : perm) { if (p >= rank()) { throw IndexError("Tensor::transpose: axis " + std::to_string(p) + " out of range (rank = " + std::to_string(rank()) + ")"); } if (seen[p]) { throw DimensionError("Tensor::transpose: duplicate axis " + std::to_string(p)); } seen[p] = true; } Tensor result; result.data_ = data_; result.offset_ = offset_; result.shape_.resize(rank()); result.strides_.resize(rank()); for (size_t i = 0; i < rank(); ++i) { result.shape_[i] = shape_[perm[i]]; result.strides_[i] = strides_[perm[i]]; } return result; } // Reverse-axis transpose (equivalent to ordinary matrix transpose for rank-2) [[nodiscard]] Tensor transpose() const { if (rank() == 0) return *this; std::vector perm(rank()); for (size_t i = 0; i < rank(); ++i) { perm[i] = rank() - 1 - i; } return transpose(perm); } // Return a contiguous copy [[nodiscard]] Tensor contiguous() const { if (isContiguous() && offset_ == 0 && data_.use_count() == 1) { return *this; // Already contiguous and uniquely owned } Tensor result(shape_); for (size_t i = 0; i < size(); ++i) { result.flat(i) = flat(i); } return result; } // ============================================================ // Arithmetic operators (compound assignment) // ============================================================ Tensor& operator+=(const Tensor& rhs) { checkSameShape(rhs, "addition"); ensureOwnership(); for (size_t i = 0; i < size(); ++i) { flat(i) = flat(i) + rhs.flat(i); } return *this; } Tensor& operator-=(const Tensor& rhs) { checkSameShape(rhs, "subtraction"); ensureOwnership(); for (size_t i = 0; i < size(); ++i) { flat(i) = flat(i) - rhs.flat(i); } return *this; } Tensor& operator*=(const T& scalar) { ensureOwnership(); for (size_t i = 0; i < size(); ++i) { flat(i) = flat(i) * scalar; } return *this; } Tensor& operator/=(const T& scalar) { if (scalar == T(0)) { throw std::invalid_argument("Tensor: division by zero"); } ensureOwnership(); for (size_t i = 0; i < size(); ++i) { flat(i) = flat(i) / scalar; } return *this; } // ============================================================ // Matrix/Vector conversion // ============================================================ // Convert a rank-2 tensor to Matrix [[nodiscard]] Matrix toMatrix() const { if (rank() != 2) { throw DimensionError("Tensor::toMatrix: rank must be 2, got " + std::to_string(rank())); } Matrix result(shape_[0], shape_[1]); for (size_t i = 0; i < shape_[0]; ++i) { for (size_t j = 0; j < shape_[1]; ++j) { size_t idx[] = { i, j }; result(i, j) = (*data_)[computeFlatIndex(std::span(idx, 2))]; } } return result; } // Convert a rank-1 tensor to Vector [[nodiscard]] Vector toVector() const { if (rank() != 1) { throw DimensionError("Tensor::toVector: rank must be 1, got " + std::to_string(rank())); } Vector result(shape_[0]); for (size_t i = 0; i < shape_[0]; ++i) { size_t idx[] = { i }; result[i] = (*data_)[computeFlatIndex(std::span(idx, 1))]; } return result; } // Convert a rank-0 tensor to a scalar [[nodiscard]] T toScalar() const { if (rank() != 0) { throw DimensionError("Tensor::toScalar: rank must be 0, got " + std::to_string(rank())); } return (*data_)[offset_]; } // ============================================================ // Output // ============================================================ [[nodiscard]] std::string to_string() const { std::ostringstream oss; oss << "Tensor(shape=["; for (size_t i = 0; i < shape_.size(); ++i) { if (i > 0) oss << ", "; oss << shape_[i]; } oss << "]"; if (isScalar()) { oss << ", value=" << (*data_)[offset_]; } else if (size() <= 20) { // When few elements, display all of them oss << ", data=["; for (size_t i = 0; i < size(); ++i) { if (i > 0) oss << ", "; oss << flat(i); } oss << "]"; } else { // When many elements, show only the head and tail oss << ", data=["; for (size_t i = 0; i < 5; ++i) { if (i > 0) oss << ", "; oss << flat(i); } oss << ", ..., "; for (size_t i = size() - 3; i < size(); ++i) { if (i > size() - 3) oss << ", "; oss << flat(i); } oss << "]"; } oss << ")"; return oss.str(); } // ============================================================ // View information (for debugging) // ============================================================ // Whether the data is shared bool isView() const noexcept { return data_.use_count() > 1; } // Reference count of the shared_ptr long useCount() const noexcept { return data_.use_count(); } private: std::shared_ptr> data_; std::vector shape_; std::vector strides_; size_t offset_ = 0; // ============================================================ // Internal utilities // ============================================================ // Flat-index computation from strides (no bounds checking) size_t computeFlatIndex(std::span indices) const { size_t flat = offset_; for (size_t i = 0; i < indices.size(); ++i) { flat += indices[i] * strides_[i]; } return flat; } // Index bounds check void checkIndices(std::span indices) const { if (indices.size() != rank()) { throw DimensionError("Tensor: expected " + std::to_string(rank()) + " indices, got " + std::to_string(indices.size())); } for (size_t i = 0; i < indices.size(); ++i) { if (indices[i] >= shape_[i]) { throw IndexError("Tensor: index[" + std::to_string(i) + "] = " + std::to_string(indices[i]) + " out of range (size = " + std::to_string(shape_[i]) + ")"); } } } // Shape equality check void checkSameShape(const Tensor& other, const std::string& op) const { if (shape_ != other.shape_) { throw DimensionError("Tensor " + op + ": shape mismatch"); } } // Convert a flat index to multi-dimensional indices (C-order) std::vector unflattenIndex(size_t flatIdx) const { std::vector indices(rank()); auto rowMajor = detail::computeRowMajorStrides(shape_); for (size_t i = 0; i < rank(); ++i) { indices[i] = flatIdx / rowMajor[i]; flatIdx %= rowMajor[i]; } return indices; } // For a view, materialize an independent contiguous copy void ensureOwnership() { if (data_.use_count() > 1 || !isContiguous() || offset_ != 0) { auto newData = std::make_shared>(size()); for (size_t i = 0; i < size(); ++i) { (*newData)[i] = flat(i); } data_ = std::move(newData); strides_ = detail::computeRowMajorStrides(shape_); offset_ = 0; } } }; // ================================================================ // Free functions: arithmetic operators // ================================================================ // Tensor addition template Tensor operator+(const Tensor& lhs, const Tensor& rhs) { Tensor result = lhs.contiguous(); result += rhs; return result; } // Tensor subtraction template Tensor operator-(const Tensor& lhs, const Tensor& rhs) { Tensor result = lhs.contiguous(); result -= rhs; return result; } // Scalar multiplication (right) template Tensor operator*(const Tensor& t, const T& scalar) { Tensor result = t.contiguous(); result *= scalar; return result; } // Scalar multiplication (left) template Tensor operator*(const T& scalar, const Tensor& t) { return t * scalar; } // Scalar division template Tensor operator/(const Tensor& t, const T& scalar) { Tensor result = t.contiguous(); result /= scalar; return result; } // Unary minus template Tensor operator-(const Tensor& t) { Tensor result(t.shape()); for (size_t i = 0; i < t.size(); ++i) { result.flat(i) = -t.flat(i); } return result; } // ================================================================ // Free functions: tensor operations // ================================================================ // Contraction: contract along the specified axis pairs // Each axes[i] = {axis_of_a, axis_of_b} pair is contracted template Tensor contract(const Tensor& a, const Tensor& b, const std::vector>& axes) { // Validate axes for (const auto& [ax_a, ax_b] : axes) { if (ax_a >= a.rank()) { throw IndexError("contract: axis_a " + std::to_string(ax_a) + " out of range (rank = " + std::to_string(a.rank()) + ")"); } if (ax_b >= b.rank()) { throw IndexError("contract: axis_b " + std::to_string(ax_b) + " out of range (rank = " + std::to_string(b.rank()) + ")"); } if (a.shape(ax_a) != b.shape(ax_b)) { throw DimensionError("contract: dimension mismatch on axes (" + std::to_string(ax_a) + ", " + std::to_string(ax_b) + "): " + std::to_string(a.shape(ax_a)) + " vs " + std::to_string(b.shape(ax_b))); } } // Classify axes as contracted vs free std::vector a_contracted(a.rank(), false); std::vector b_contracted(b.rank(), false); for (const auto& [ax_a, ax_b] : axes) { a_contracted[ax_a] = true; b_contracted[ax_b] = true; } // Result shape: a's free axes + b's free axes std::vector result_shape; std::vector a_free_axes, b_free_axes; for (size_t i = 0; i < a.rank(); ++i) { if (!a_contracted[i]) { a_free_axes.push_back(i); result_shape.push_back(a.shape(i)); } } for (size_t i = 0; i < b.rank(); ++i) { if (!b_contracted[i]) { b_free_axes.push_back(i); result_shape.push_back(b.shape(i)); } } // Sizes of the contracted axes (product over all pairs) std::vector contract_axes_a, contract_axes_b; std::vector contract_sizes; for (const auto& [ax_a, ax_b] : axes) { contract_axes_a.push_back(ax_a); contract_axes_b.push_back(ax_b); contract_sizes.push_back(a.shape(ax_a)); } // Case where the result becomes rank-0 if (result_shape.empty()) { // All axes are contracted → scalar result T sum = T(0); // Iterate over the contracted axes size_t contract_total = 1; for (size_t s : contract_sizes) contract_total *= s; for (size_t ci = 0; ci < contract_total; ++ci) { // Unpack the contracted indices std::vector cidx(contract_sizes.size()); size_t tmp = ci; for (size_t k = contract_sizes.size(); k > 0; --k) { cidx[k - 1] = tmp % contract_sizes[k - 1]; tmp /= contract_sizes[k - 1]; } // Build the index for a std::vector a_idx(a.rank()); for (size_t k = 0; k < contract_axes_a.size(); ++k) { a_idx[contract_axes_a[k]] = cidx[k]; } // a's free axes are all empty (rank-0, so no free axes) // Build the index for b std::vector b_idx(b.rank()); for (size_t k = 0; k < contract_axes_b.size(); ++k) { b_idx[contract_axes_b[k]] = cidx[k]; } sum = sum + a.at(std::span(a_idx)) * b.at(std::span(b_idx)); } return Tensor(sum); } Tensor result(result_shape); // Loop over every element of the result size_t result_total = result.size(); auto result_strides = detail::computeRowMajorStrides(result_shape); // Total number of contracted-axis combinations size_t contract_total = 1; for (size_t s : contract_sizes) contract_total *= s; for (size_t ri = 0; ri < result_total; ++ri) { // Unpack the result's flat index into multi-dimensional indices std::vector r_idx(result_shape.size()); size_t tmp = ri; for (size_t k = result_shape.size(); k > 0; --k) { r_idx[k - 1] = tmp % result_shape[k - 1]; tmp /= result_shape[k - 1]; } // Split r_idx into a_free and b_free parts T sum = T(0); for (size_t ci = 0; ci < contract_total; ++ci) { // Unpack the contracted indices std::vector cidx(contract_sizes.size()); tmp = ci; for (size_t k = contract_sizes.size(); k > 0; --k) { cidx[k - 1] = tmp % contract_sizes[k - 1]; tmp /= contract_sizes[k - 1]; } // Build the index for a std::vector a_idx(a.rank()); for (size_t k = 0; k < a_free_axes.size(); ++k) { a_idx[a_free_axes[k]] = r_idx[k]; } for (size_t k = 0; k < contract_axes_a.size(); ++k) { a_idx[contract_axes_a[k]] = cidx[k]; } // Build the index for b std::vector b_idx(b.rank()); for (size_t k = 0; k < b_free_axes.size(); ++k) { b_idx[b_free_axes[k]] = r_idx[a_free_axes.size() + k]; } for (size_t k = 0; k < contract_axes_b.size(); ++k) { b_idx[contract_axes_b[k]] = cidx[k]; } sum = sum + a.at(std::span(a_idx)) * b.at(std::span(b_idx)); } result.at(std::span(r_idx)) = sum; } return result; } // Tensor product (outer product): rank(result) = rank(a) + rank(b) template Tensor outerProduct(const Tensor& a, const Tensor& b) { // Result shape = a.shape + b.shape std::vector result_shape; result_shape.reserve(a.rank() + b.rank()); for (size_t i = 0; i < a.rank(); ++i) result_shape.push_back(a.shape(i)); for (size_t i = 0; i < b.rank(); ++i) result_shape.push_back(b.shape(i)); // Both rank-0: scalar product if (result_shape.empty()) { return Tensor(a.toScalar() * b.toScalar()); } Tensor result(result_shape); size_t a_size = a.size(); size_t b_size = b.size(); for (size_t i = 0; i < a_size; ++i) { for (size_t j = 0; j < b_size; ++j) { result.flat(i * b_size + j) = a.flat(i) * b.flat(j); } } return result; } // Hadamard product (element-wise product) template Tensor hadamard(const Tensor& a, const Tensor& b) { if (a.shape() != b.shape()) { throw DimensionError("hadamard: shape mismatch"); } Tensor result(a.shape()); for (size_t i = 0; i < a.size(); ++i) { result.flat(i) = a.flat(i) * b.flat(i); } return result; } // ================================================================ // Factory functions // ================================================================ namespace tensor { // Zero tensor template Tensor zeros(std::vector shape) { return Tensor(std::move(shape), T(0)); } // Tensor of all ones template Tensor ones(std::vector shape) { return Tensor(std::move(shape), T(1)); } } // ================================================================ // Stream output // ================================================================ template std::ostream& operator<<(std::ostream& os, const Tensor& t) { os << t.to_string(); return os; } // ================================================================ // Type traits // ================================================================ // Traits that determine whether a type is a Tensor template struct is_tensor : std::false_type {}; template struct is_tensor> : std::true_type {}; template inline constexpr bool is_tensor_v = is_tensor::value; } // namespace sangi #endif // SANGI_TENSOR_HPP