ENG | Eigen Pitfalls: Hidden danger of lazy evaluation
Article was mostly written by Google Gemini, based on my experience.
If you are using the Eigen library for linear algebra in C++, you might eventually encounter a ghost: code that runs perfectly on your machine during development but produces garbage values the moment you switch to a Release build.
The culprit? A combination of C++11’s auto keyword and Eigen’s Expression Templates.
The Problematic Code
Consider this snippet where we transform a vertex by a matrix generated on the fly:
1
2
auto result_bad = toGlobalMatrix() * v;
std::cout << result_bad << std::endl;
On the surface, it looks like standard C++. However, this is a memory safety nightmare.
What is happening under the hood?
Eigen uses Expression Templates to achieve high performance. When you write matrix * vector, Eigen doesn’t actually perform the multiplication immediately. Instead, it returns a “proxy object” (a Product expression) that says: “I will calculate this later when you actually need the numbers.”
To avoid heavy copying, this proxy object stores references to the matrix and the vector.
- The Temporary:
toGlobalMatrix()returns a temporaryMatrix4dby value. - The Reference: The
autokeyword captures the proxy expression, which holds a reference to that temporary matrix. - The Death: At the end of the line (the semicolon), the temporary matrix is destroyed.
- The Dangling Reference: When you finally print or use
result_bad, Eigen tries to read the memory of the already-deleted matrix.
Why does it work in Debug?
In Debug mode, compilers like GCC or Clang usually don’t reuse stack memory immediately. The “dead” matrix stays in memory long enough for you to print it. In Release mode (-O3), the compiler optimizes the stack, reuses that memory for something else leading to the data corruption and/or crash.
How to Fix It
You have two reliable ways to force evaluation while the temporary objects are still alive:
1. Explicit Typing (Recommended)
By declaring the type explicitly, you force the expression to evaluate and store the result in a concrete object.
1
Eigen::Vector4d result_typed = toGlobalMatrix() * v;
2. Using .eval()
If you must use auto, use the .eval() method to trigger the computation immediately.
1
auto result_eval = (toGlobalMatrix() * v).eval();
Summary
Never use auto to capture an Eigen expression that involves a temporary (rvalue). The expression template holds a reference to it, and by the next statement the temporary is already destroyed. If you see a Release-only bug in your math code, look for dangling references to dead temporaries first.
Full example
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
cmake_minimum_required(VERSION 3.20)
project(eigen_lifetime_test)
set(CMAKE_CXX_STANDARD 17)
include(FetchContent)
FetchContent_Declare(
eigen
GIT_REPOSITORY https://gitlab.com/libeigen/eigen.git
GIT_TAG 3.4.0
GIT_SHALLOW TRUE
)
FetchContent_MakeAvailable(eigen)
add_executable(eigen_lifetime_test main.cpp)
target_link_libraries(eigen_lifetime_test PRIVATE Eigen3::Eigen)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
#include <Eigen/Core>
#include <cstring>
#include <iostream>
Eigen::Matrix4d toGlobalMatrix() {
Eigen::Matrix4d m = Eigen::Matrix4d::Identity();
m(0, 3) = 1.0;
m(1, 3) = 2.0;
return m;
}
Eigen::Vector4d getVertex() {
return Eigen::Vector4d(1.0, 0.0, 0.0, 1.0);
}
int main() {
Eigen::Vector4d v = getVertex();
// --- UNDEFINED BEHAVIOR ---
// Why this is dangerous:
// toGlobalMatrix() returns a temporary (rvalue). Eigen's operator* doesn't
// perform math yet; it returns a 'Product' expression object that holds
// a REFERENCE to that temporary. By the time 'result_bad' is used,
// the temporary matrix is destroyed.
//
// Note: This often "works" in Debug because the stack isn't cleaned up,
// but GCC Release mode will reuse this memory, causing a crash or junk data.
auto result_bad = toGlobalMatrix() * v;
std::cout << "Undefined behaviour, especially dangerous in release build\n"
<< result_bad << "\n\n";
// --- SAFE: Manual Evaluation ---
// .eval() forces Eigen to compute the result immediately and store it
// in a new temporary Vector4d. 'auto' then captures that vector by value.
auto result_eval = (toGlobalMatrix() * v).eval();
std::cout << "Safe (.eval()):\n" << result_eval << "\n\n";
// --- SAFE: Type-Driven Evaluation ---
// Assigning to a concrete Eigen type (Vector4d) instead of 'auto'
// triggers the expression evaluator immediately while the temporary
// matrix from toGlobalMatrix() is still alive on this line.
Eigen::Vector4d result_typed = toGlobalMatrix() * v;
std::cout << "Safe (explicit type):\n" << result_typed << "\n\n";
return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
[pavel@marten -=- ~/claude-projects/eigen-test]$ cmake -B build -S . -DCMAKE_BUILD_TYPE=RelWithDebInfo
-- Configuring done (11.3s)
-- Generating done (2.8s)
-- Build files have been written to: /home/pavel/claude-projects/eigen-test/build
[pavel@marten -=- ~/claude-projects/eigen-test]$ cmake --build build
Building CXX object CMakeFiles/eigen_lifetime_test.dir/main.cpp.o
Linking CXX executable eigen_lifetime_test
Built target eigen_lifetime_test
[pavel@marten -=- ~/claude-projects/eigen-test]$ build/eigen_lifetime_test
Undefined behaviour, especially dangerous in release build
-2.60696e-40
-0.120083
-6.05398e+37
1.38108e-309
Safe (.eval()):
2
2
0
1
Safe (explicit type):
2
2
0
1
