Add C++17 structured bindings support for fixed-size Matrix and Array libeigen/eigen!2336 Closes #2247
diff --git a/Eigen/Core b/Eigen/Core index 424396f..2fec8f3 100644 --- a/Eigen/Core +++ b/Eigen/Core
@@ -361,6 +361,7 @@ #include "src/Core/PlainObjectBase.h" #include "src/Core/Matrix.h" #include "src/Core/Array.h" +#include "src/Core/StructuredBindings.h" #include "src/Core/Fill.h" #include "src/Core/CwiseTernaryOp.h" #include "src/Core/CwiseBinaryOp.h"
diff --git a/Eigen/src/Core/StructuredBindings.h b/Eigen/src/Core/StructuredBindings.h new file mode 100644 index 0000000..073f10e --- /dev/null +++ b/Eigen/src/Core/StructuredBindings.h
@@ -0,0 +1,155 @@ +// This file is part of Eigen, a lightweight C++ template library +// for linear algebra. +// +// Copyright (C) 2026 Pavel Guzenfeld +// +// This Source Code Form is subject to the terms of the Mozilla +// Public License v. 2.0. If a copy of the MPL was not distributed +// with this file, You can obtain one at http://mozilla.org/MPL/2.0/. + +#ifndef EIGEN_STRUCTURED_BINDINGS_H +#define EIGEN_STRUCTURED_BINDINGS_H + +// IWYU pragma: private +#include "./InternalHeaderCheck.h" + +#if EIGEN_MAX_CPP_VER >= 17 && EIGEN_COMP_CXXVER >= 17 + +#include <tuple> +#include <type_traits> + +// Structured bindings support for fixed-size Eigen vectors and matrices. +// +// Enables: +// Eigen::Vector3d v(1, 2, 3); +// auto [x, y, z] = v; +// +// Eigen::Array3i a(4, 5, 6); +// auto& [a0, a1, a2] = a; +// +// Decomposition order follows storage order: column-major by default, +// so Matrix2d decomposes as (0,0), (1,0), (0,1), (1,1). Only fixed-size +// column-major Matrix and Array specialize here; Map, Ref, and fixed-size +// Block intentionally do not participate. + +namespace std { + +// std::tuple_size for fixed-size Matrix. +// +// Deliberately NOT SFINAE-gated on (Rows, Cols) because base-class-specifier +// substitution is not a SFINAE context (a malformed base via enable_if_t +// produces a non-SFINAE hard error rather than letting the primary template +// stay incomplete). The static_assert below produces a friendly diagnostic +// if generic code probes tuple_size<MatrixXd>. +template <typename Scalar_, int Rows_, int Cols_, int Options_, int MaxRows_, int MaxCols_> +struct tuple_size<Eigen::Matrix<Scalar_, Rows_, Cols_, Options_, MaxRows_, MaxCols_>> + : std::integral_constant<size_t, static_cast<size_t>((Rows_ > 0 && Cols_ > 0) ? Rows_* Cols_ : 0)> { + static_assert(Rows_ != Eigen::Dynamic && Cols_ != Eigen::Dynamic, + "Structured bindings require fixed-size Eigen types (e.g. Vector3d, not VectorXd)."); +}; + +// std::tuple_element for fixed-size Matrix. +// Note: uses Idx_ instead of I to avoid conflict with Eigen's test framework macro. +template <size_t Idx_, typename Scalar_, int Rows_, int Cols_, int Options_, int MaxRows_, int MaxCols_> +struct tuple_element<Idx_, Eigen::Matrix<Scalar_, Rows_, Cols_, Options_, MaxRows_, MaxCols_>> { + static_assert(Rows_ != Eigen::Dynamic && Cols_ != Eigen::Dynamic, + "Structured bindings require fixed-size Eigen types (e.g. Vector3d, not VectorXd)."); + static_assert(Idx_ < static_cast<size_t>(Rows_ * Cols_), "Index out of range."); + using type = Scalar_; +}; + +// std::tuple_size for fixed-size Array. See note on Matrix specialization above. +template <typename Scalar_, int Rows_, int Cols_, int Options_, int MaxRows_, int MaxCols_> +struct tuple_size<Eigen::Array<Scalar_, Rows_, Cols_, Options_, MaxRows_, MaxCols_>> + : std::integral_constant<size_t, static_cast<size_t>((Rows_ > 0 && Cols_ > 0) ? Rows_* Cols_ : 0)> { + static_assert(Rows_ != Eigen::Dynamic && Cols_ != Eigen::Dynamic, + "Structured bindings require fixed-size Eigen types (e.g. Array3d, not ArrayXd)."); +}; + +// std::tuple_element for fixed-size Array. +template <size_t Idx_, typename Scalar_, int Rows_, int Cols_, int Options_, int MaxRows_, int MaxCols_> +struct tuple_element<Idx_, Eigen::Array<Scalar_, Rows_, Cols_, Options_, MaxRows_, MaxCols_>> { + static_assert(Rows_ != Eigen::Dynamic && Cols_ != Eigen::Dynamic, + "Structured bindings require fixed-size Eigen types (e.g. Array3d, not ArrayXd)."); + static_assert(Idx_ < static_cast<size_t>(Rows_ * Cols_), "Index out of range."); + using type = Scalar_; +}; + +} // namespace std + +namespace Eigen { + +// Until the decomposition order for genuinely 2D RowMajor storage is agreed +// upon, reject row-major matrices at the get<I> level. coeffRef(Index) is +// linear in storage order, so permitting both orientations for 2D types would +// silently flip the binding order between Matrix<T,R,C> and Matrix<T,R,C,RowMajor>. +// Vector types (Rows==1 or Cols==1) are unaffected because storage order +// does not change element order for a 1×N or N×1 shape — and Eigen forces +// row-major on 1×N regardless, so we must allow it for row vectors. +#define EIGEN_STRUCTURED_BINDINGS_ASSERT_COL_MAJOR(ROWS, COLS, OPTIONS) \ + static_assert((ROWS) == 1 || (COLS) == 1 || ((OPTIONS) & Eigen::RowMajorBit) == 0, \ + "Structured bindings on 2D RowMajor Eigen types are not supported: coeffRef(Index) follows " \ + "storage order, so decomposition order would silently flip versus the column-major default. " \ + "Use a column-major type, or transpose first. Row/column vectors are unaffected.") + +// get<Idx_> free functions for Matrix. +template <size_t Idx_, typename Scalar_, int Rows_, int Cols_, int Options_, int MaxRows_, int MaxCols_> +EIGEN_DEVICE_FUNC EIGEN_STRONG_INLINE Scalar_& get( + Matrix<Scalar_, Rows_, Cols_, Options_, MaxRows_, MaxCols_>& m) noexcept { + static_assert(Rows_ != Dynamic && Cols_ != Dynamic, "Structured bindings require fixed-size Eigen types."); + static_assert(Idx_ < static_cast<size_t>(Rows_ * Cols_), "Index out of range."); + EIGEN_STRUCTURED_BINDINGS_ASSERT_COL_MAJOR(Rows_, Cols_, Options_); + return m.coeffRef(static_cast<Index>(Idx_)); +} + +template <size_t Idx_, typename Scalar_, int Rows_, int Cols_, int Options_, int MaxRows_, int MaxCols_> +EIGEN_DEVICE_FUNC EIGEN_STRONG_INLINE const Scalar_& get( + const Matrix<Scalar_, Rows_, Cols_, Options_, MaxRows_, MaxCols_>& m) noexcept { + static_assert(Rows_ != Dynamic && Cols_ != Dynamic, "Structured bindings require fixed-size Eigen types."); + static_assert(Idx_ < static_cast<size_t>(Rows_ * Cols_), "Index out of range."); + EIGEN_STRUCTURED_BINDINGS_ASSERT_COL_MAJOR(Rows_, Cols_, Options_); + return m.coeffRef(static_cast<Index>(Idx_)); +} + +template <size_t Idx_, typename Scalar_, int Rows_, int Cols_, int Options_, int MaxRows_, int MaxCols_> +EIGEN_DEVICE_FUNC EIGEN_STRONG_INLINE Scalar_&& get( + Matrix<Scalar_, Rows_, Cols_, Options_, MaxRows_, MaxCols_>&& m) noexcept { + static_assert(Rows_ != Dynamic && Cols_ != Dynamic, "Structured bindings require fixed-size Eigen types."); + static_assert(Idx_ < static_cast<size_t>(Rows_ * Cols_), "Index out of range."); + EIGEN_STRUCTURED_BINDINGS_ASSERT_COL_MAJOR(Rows_, Cols_, Options_); + return std::move(m.coeffRef(static_cast<Index>(Idx_))); +} + +// get<Idx_> free functions for Array. +template <size_t Idx_, typename Scalar_, int Rows_, int Cols_, int Options_, int MaxRows_, int MaxCols_> +EIGEN_DEVICE_FUNC EIGEN_STRONG_INLINE Scalar_& get( + Array<Scalar_, Rows_, Cols_, Options_, MaxRows_, MaxCols_>& a) noexcept { + static_assert(Rows_ != Dynamic && Cols_ != Dynamic, "Structured bindings require fixed-size Eigen types."); + static_assert(Idx_ < static_cast<size_t>(Rows_ * Cols_), "Index out of range."); + EIGEN_STRUCTURED_BINDINGS_ASSERT_COL_MAJOR(Rows_, Cols_, Options_); + return a.coeffRef(static_cast<Index>(Idx_)); +} + +template <size_t Idx_, typename Scalar_, int Rows_, int Cols_, int Options_, int MaxRows_, int MaxCols_> +EIGEN_DEVICE_FUNC EIGEN_STRONG_INLINE const Scalar_& get( + const Array<Scalar_, Rows_, Cols_, Options_, MaxRows_, MaxCols_>& a) noexcept { + static_assert(Rows_ != Dynamic && Cols_ != Dynamic, "Structured bindings require fixed-size Eigen types."); + static_assert(Idx_ < static_cast<size_t>(Rows_ * Cols_), "Index out of range."); + EIGEN_STRUCTURED_BINDINGS_ASSERT_COL_MAJOR(Rows_, Cols_, Options_); + return a.coeffRef(static_cast<Index>(Idx_)); +} + +template <size_t Idx_, typename Scalar_, int Rows_, int Cols_, int Options_, int MaxRows_, int MaxCols_> +EIGEN_DEVICE_FUNC EIGEN_STRONG_INLINE Scalar_&& get( + Array<Scalar_, Rows_, Cols_, Options_, MaxRows_, MaxCols_>&& a) noexcept { + static_assert(Rows_ != Dynamic && Cols_ != Dynamic, "Structured bindings require fixed-size Eigen types."); + static_assert(Idx_ < static_cast<size_t>(Rows_ * Cols_), "Index out of range."); + EIGEN_STRUCTURED_BINDINGS_ASSERT_COL_MAJOR(Rows_, Cols_, Options_); + return std::move(a.coeffRef(static_cast<Index>(Idx_))); +} + +} // namespace Eigen + +#endif // C++17 + +#endif // EIGEN_STRUCTURED_BINDINGS_H
diff --git a/failtest/CMakeLists.txt b/failtest/CMakeLists.txt index 107c9e2..0e55c0b 100644 --- a/failtest/CMakeLists.txt +++ b/failtest/CMakeLists.txt
@@ -66,3 +66,20 @@ ei_add_failtest("eigensolver_cplx") ei_add_failtest("initializer_list_1") ei_add_failtest("initializer_list_2") + +ei_add_failtest("structured_bindings_dynamic_matrix") +ei_add_failtest("structured_bindings_dynamic_array") +ei_add_failtest("structured_bindings_rowmajor") + +# Structured bindings require C++17 — the header is a no-op otherwise and the +# "_ok" targets would not compile. Force the standard on these four targets +# (two per failtest: the _ok and _ko variants). +foreach(_sb_target + structured_bindings_dynamic_matrix_ok structured_bindings_dynamic_matrix_ko + structured_bindings_dynamic_array_ok structured_bindings_dynamic_array_ko + structured_bindings_rowmajor_ok structured_bindings_rowmajor_ko) + set_target_properties(${_sb_target} PROPERTIES + CXX_STANDARD 17 + CXX_STANDARD_REQUIRED ON + CXX_EXTENSIONS OFF) +endforeach()
diff --git a/failtest/structured_bindings_dynamic_array.cpp b/failtest/structured_bindings_dynamic_array.cpp new file mode 100644 index 0000000..1986e5c --- /dev/null +++ b/failtest/structured_bindings_dynamic_array.cpp
@@ -0,0 +1,16 @@ +#include "../Eigen/Core" + +// Reproduces the "Dynamic-sized Array breaks tuple_size" bug: the Array +// specialization had the same enable_if_t base-clause issue as Matrix. Compile +// must fail for ArrayXd. +#ifdef EIGEN_SHOULD_FAIL_TO_BUILD +#define ROWS Eigen::Dynamic +#define COLS Eigen::Dynamic +#else +#define ROWS 3 +#define COLS 1 +#endif + +#include <tuple> + +int main() { return static_cast<int>(std::tuple_size<Eigen::Array<double, ROWS, COLS>>::value); }
diff --git a/failtest/structured_bindings_dynamic_matrix.cpp b/failtest/structured_bindings_dynamic_matrix.cpp new file mode 100644 index 0000000..b571112 --- /dev/null +++ b/failtest/structured_bindings_dynamic_matrix.cpp
@@ -0,0 +1,17 @@ +#include "../Eigen/Core" + +// Reproduces the "Dynamic-sized Matrix breaks tuple_size" bug reported on !2336. +// With an enable_if_t base-clause the error is a cryptic non-SFINAE hard error; +// with our static_assert-in-body fix it becomes a friendly diagnostic. Either +// way, the compile must fail for MatrixXd. +#ifdef EIGEN_SHOULD_FAIL_TO_BUILD +#define ROWS Eigen::Dynamic +#define COLS Eigen::Dynamic +#else +#define ROWS 3 +#define COLS 1 +#endif + +#include <tuple> + +int main() { return static_cast<int>(std::tuple_size<Eigen::Matrix<double, ROWS, COLS>>::value); }
diff --git a/failtest/structured_bindings_rowmajor.cpp b/failtest/structured_bindings_rowmajor.cpp new file mode 100644 index 0000000..a3c502a --- /dev/null +++ b/failtest/structured_bindings_rowmajor.cpp
@@ -0,0 +1,17 @@ +#include "../Eigen/Core" + +// Reproduces the "storage-order footgun" on !2336: get<I>() uses coeffRef(Index), +// which is linear in storage order, so a RowMajor matrix would silently flip +// decomposition order vs. the column-major default. We static_assert against +// RowMajor until the semantics are agreed upon — this failtest guards that. +#ifdef EIGEN_SHOULD_FAIL_TO_BUILD +#define STORAGE_ORDER Eigen::RowMajor +#else +#define STORAGE_ORDER Eigen::ColMajor +#endif + +int main() { + Eigen::Matrix<double, 2, 2, STORAGE_ORDER> m; + m << 1, 2, 3, 4; + return static_cast<int>(Eigen::get<0>(m)); +}
diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index b362f1f..8d551b0 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt
@@ -338,6 +338,7 @@ ei_add_test(diagonal_matrix_variadic_ctor) ei_add_test(serializer) ei_add_test(tuple_test) +ei_add_test(structured_bindings) ei_add_test(threads_eventcount "-pthread" "${CMAKE_THREAD_LIBS_INIT}") ei_add_test(threads_runqueue "-pthread" "${CMAKE_THREAD_LIBS_INIT}") ei_add_test(threads_non_blocking_thread_pool "-pthread" "${CMAKE_THREAD_LIBS_INIT}")
diff --git a/test/structured_bindings.cpp b/test/structured_bindings.cpp new file mode 100644 index 0000000..e497884 --- /dev/null +++ b/test/structured_bindings.cpp
@@ -0,0 +1,213 @@ +// This file is part of Eigen, a lightweight C++ template library +// for linear algebra. +// +// Copyright (C) 2026 Pavel Guzenfeld +// +// This Source Code Form is subject to the terms of the Mozilla +// Public License v. 2.0. If a copy of the MPL was not distributed +// with this file, You can obtain one at http://mozilla.org/MPL/2.0/. + +#include "main.h" + +#if EIGEN_MAX_CPP_VER >= 17 && EIGEN_COMP_CXXVER >= 17 + +template <typename Scalar> +void check_vector_bindings() { + // Vector2 + { + Matrix<Scalar, 2, 1> v; + v << Scalar(1), Scalar(2); + auto [x, y] = v; + VERIFY_IS_EQUAL(x, Scalar(1)); + VERIFY_IS_EQUAL(y, Scalar(2)); + } + + // Vector3 + { + Matrix<Scalar, 3, 1> v; + v << Scalar(3), Scalar(4), Scalar(5); + auto [x, y, z] = v; + VERIFY_IS_EQUAL(x, Scalar(3)); + VERIFY_IS_EQUAL(y, Scalar(4)); + VERIFY_IS_EQUAL(z, Scalar(5)); + } + + // Vector4 + { + Matrix<Scalar, 4, 1> v; + v << Scalar(6), Scalar(7), Scalar(8), Scalar(9); + auto [a, b, c, d] = v; + VERIFY_IS_EQUAL(a, Scalar(6)); + VERIFY_IS_EQUAL(b, Scalar(7)); + VERIFY_IS_EQUAL(c, Scalar(8)); + VERIFY_IS_EQUAL(d, Scalar(9)); + } + + // 1x1 matrix (scalar-like) + { + Matrix<Scalar, 1, 1> s; + s << Scalar(42); + auto [val] = s; + VERIFY_IS_EQUAL(val, Scalar(42)); + } + + // RowVector + { + Matrix<Scalar, 1, 3> rv; + rv << Scalar(10), Scalar(20), Scalar(30); + auto [a, b, c] = rv; + VERIFY_IS_EQUAL(a, Scalar(10)); + VERIFY_IS_EQUAL(b, Scalar(20)); + VERIFY_IS_EQUAL(c, Scalar(30)); + } +} + +template <typename Scalar> +void check_array_bindings() { + // Array3 + { + Array<Scalar, 3, 1> a; + a << Scalar(100), Scalar(200), Scalar(300); + auto [x, y, z] = a; + VERIFY_IS_EQUAL(x, Scalar(100)); + VERIFY_IS_EQUAL(y, Scalar(200)); + VERIFY_IS_EQUAL(z, Scalar(300)); + } + + // Array2 + { + Array<Scalar, 2, 1> a; + a << Scalar(10), Scalar(20); + auto [x, y] = a; + VERIFY_IS_EQUAL(x, Scalar(10)); + VERIFY_IS_EQUAL(y, Scalar(20)); + } +} + +template <typename Scalar> +void check_reference_bindings() { + // Mutable reference binding + { + Matrix<Scalar, 3, 1> v; + v << Scalar(1), Scalar(2), Scalar(3); + auto& [x, y, z] = v; + x = Scalar(10); + y = Scalar(20); + z = Scalar(30); + VERIFY_IS_EQUAL(v(0), Scalar(10)); + VERIFY_IS_EQUAL(v(1), Scalar(20)); + VERIFY_IS_EQUAL(v(2), Scalar(30)); + } + + // Const reference binding + { + const Matrix<Scalar, 3, 1> v(Scalar(4), Scalar(5), Scalar(6)); + const auto& [x, y, z] = v; + VERIFY_IS_EQUAL(x, Scalar(4)); + VERIFY_IS_EQUAL(y, Scalar(5)); + VERIFY_IS_EQUAL(z, Scalar(6)); + } + + // Array mutable reference binding + { + Array<Scalar, 2, 1> a; + a << Scalar(7), Scalar(8); + auto& [x, y] = a; + x = Scalar(70); + VERIFY_IS_EQUAL(a(0), Scalar(70)); + VERIFY_IS_EQUAL(a(1), Scalar(8)); + } +} + +template <typename Scalar> +void check_matrix_bindings() { + // 2x2 matrix (column-major order) + { + Matrix<Scalar, 2, 2> m; + m << Scalar(1), Scalar(2), Scalar(3), Scalar(4); + auto [m00, m10, m01, m11] = m; + // Column-major: (0,0), (1,0), (0,1), (1,1) + VERIFY_IS_EQUAL(m00, Scalar(1)); + VERIFY_IS_EQUAL(m10, Scalar(3)); + VERIFY_IS_EQUAL(m01, Scalar(2)); + VERIFY_IS_EQUAL(m11, Scalar(4)); + } +} + +template <typename Scalar> +void check_storage_order_semantics() { + // Row vectors are forced to RowMajor by Eigen (a 1xN can't meaningfully be + // column-major). Ensure decomposition still matches the single-row layout. + { + Matrix<Scalar, 1, 3> rv; + rv << Scalar(1), Scalar(2), Scalar(3); + auto [a, b, c] = rv; + VERIFY_IS_EQUAL(a, Scalar(1)); + VERIFY_IS_EQUAL(b, Scalar(2)); + VERIFY_IS_EQUAL(c, Scalar(3)); + } + // Nx1 column vectors are ColMajor regardless and decompose top-to-bottom. + { + Matrix<Scalar, 3, 1> cv; + cv << Scalar(4), Scalar(5), Scalar(6); + auto [a, b, c] = cv; + VERIFY_IS_EQUAL(a, Scalar(4)); + VERIFY_IS_EQUAL(b, Scalar(5)); + VERIFY_IS_EQUAL(c, Scalar(6)); + } + // 2D ColMajor matrix decomposes in column-major order: (0,0),(1,0),(0,1),(1,1). + // 2D RowMajor is rejected via static_assert — see failtest/structured_bindings_rowmajor.cpp. + { + Matrix<Scalar, 2, 2, ColMajor> m; + m << Scalar(1), Scalar(2), Scalar(3), Scalar(4); + auto [m00, m10, m01, m11] = m; + VERIFY_IS_EQUAL(m00, Scalar(1)); + VERIFY_IS_EQUAL(m10, Scalar(3)); + VERIFY_IS_EQUAL(m01, Scalar(2)); + VERIFY_IS_EQUAL(m11, Scalar(4)); + } +} + +void check_tuple_size() { + STATIC_CHECK((std::tuple_size<Vector2d>::value == 2)); + STATIC_CHECK((std::tuple_size<Vector3f>::value == 3)); + STATIC_CHECK((std::tuple_size<Vector4i>::value == 4)); + STATIC_CHECK((std::tuple_size<Matrix2d>::value == 4)); + STATIC_CHECK((std::tuple_size<Matrix3f>::value == 9)); + STATIC_CHECK((std::tuple_size<Array3i>::value == 3)); + STATIC_CHECK((std::tuple_size<Array<double, 2, 1>>::value == 2)); + STATIC_CHECK((std::tuple_size<Matrix<float, 1, 1>>::value == 1)); + STATIC_CHECK((std::tuple_size<RowVector3d>::value == 3)); +} + +void check_tuple_element() { + STATIC_CHECK((std::is_same<std::tuple_element_t<0, Vector3d>, double>::value)); + STATIC_CHECK((std::is_same<std::tuple_element_t<1, Vector3f>, float>::value)); + STATIC_CHECK((std::is_same<std::tuple_element_t<2, Vector4i>, int>::value)); + STATIC_CHECK((std::is_same<std::tuple_element_t<0, Array3i>, int>::value)); +} + +EIGEN_DECLARE_TEST(structured_bindings) { + CALL_SUBTEST_1(check_vector_bindings<double>()); + CALL_SUBTEST_1(check_vector_bindings<float>()); + CALL_SUBTEST_1(check_vector_bindings<int>()); + CALL_SUBTEST_2(check_array_bindings<double>()); + CALL_SUBTEST_2(check_array_bindings<int>()); + CALL_SUBTEST_3(check_reference_bindings<double>()); + CALL_SUBTEST_3(check_reference_bindings<float>()); + CALL_SUBTEST_4(check_matrix_bindings<double>()); + CALL_SUBTEST_4(check_matrix_bindings<int>()); + CALL_SUBTEST_5(check_tuple_size()); + CALL_SUBTEST_5(check_tuple_element()); + CALL_SUBTEST_6(check_storage_order_semantics<double>()); + CALL_SUBTEST_6(check_storage_order_semantics<int>()); +} + +#else + +EIGEN_DECLARE_TEST(structured_bindings) { + // Structured bindings require C++17. + VERIFY(true); +} + +#endif