ARTICLE AD BOX
I encountered a very odd issue in a larger project and after a lot of debugging reduced it to this minimal example. My idea is that a long comma-expression gets truncated after hitting some threshold.
#include <cstddef> #include <iostream> struct Counter { // Counts number of `operator,` calls std::size_t count = 0; Counter& operator,(int) { ++count; return *this; } void finish(const char* label, std::size_t expected) const { std::cout << label << ": count=" << count << " expected=" << expected << "\n"; } }; int main() { Counter c129; c129, // an array of 129 zeros 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0; c129.finish("129", 129); Counter c130; c130, // an array of 130 zeros 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0; c130.finish("130", 130); return 0; }This outputs
129: count=129 expected=129 130: count=3 expected=130Which suggests operator, is only called 3 times in the second case. I also verified this by some other logs. Ultimately, in my case this means in the following code the array never gets initialized fully without a single warning
#include <Eigen/Dense> #include <iostream> int main() { Eigen::VectorXd adj; adj.resize(129); adj << // and array of 129 zeros 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0; std::cout << "Adjacency Matrix:\n" << adj.transpose() << std::endl; std::cout << "Worked perfectly!" << std::endl; std::cout << "This does NOT work:" << std::endl; Eigen::VectorXd adj_2; adj_2.resize(130); adj_2 << // and array of 130 zeros 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0; std::cout << "Adjacency Matrix 2:\n" << adj_2.transpose() << std::endl; return 0; }Looking into the GIMPLE tree-dump of the first code, I noticed that the 129 version compiles as expected
struct Counter c129; try { c129.count = 0; _1 = Counter::operator, (&c129, 0); _2 = Counter::operator, (_1, 0); _3 = Counter::operator, (_2, 0); ... _128 = Counter::operator, (_127, 0); Counter::operator, (_128, 0); Counter::finish (&c129, "129", 129); } finally { c129 = {CLOBBER(eos)}; }But for the 130 chain, the dump appears to be truncated
struct Counter c130; try { c130.count = 0; _1 = Counter::operator, (&c130, 0); _2 = Counter::operator, (_1, 0); Counter::operator, (_2, 0); Counter::finish (&c130, "130", 130); } finally { c130 = {CLOBBER(eos)}; }Which I think means the truncation happens in the front-end before any optimization. I also checked this with many different sets of flags, including -O0 -fno-inline -fno-inline-functions -fno-inline-small-functions and ultimately this consistent.
This happens in GCC 15.2.1 on cpp standards that I tested (03 to 23), but Clang 21.1.6 does not produce the wrong result for the sizes I tested (up to 10000). If I increase the chain length a lot, Clang eventually says
warning: stack nearly exhausted; compilation time may suffer, and crashes due to stack overflow are likely [-Wstack-exhausted]Also I did not test any other GCC version, but 15.2.1 is relatively new
In my original project, this was caused via Eigen's comma initializer pattern
Eigen::MatrixXd adj(N, M); adj << 0, 1, ...;as Eigen's CommaInitializer is implemented similarly to Counter class. Also again, this was extremely silent, no warning or error from GCC, and not a single assert caught this early, just wrong runtime behavior even worse with optimizations on.
So my questions are,
Is this a known GCC limitation/bug (like expression depth limit / compiler or parse stack) that could lead to silent wrong code generation?
Does this look like a GCC bug worth reporting? Or is it just undefined behavior written somewhere?
EDIT: From Compiler Explorer I noticed this happens only with GCC 15 series and does not cause any issues with older versions.
Also, defining std::size_t count as a static member also causes the same issue.
Original Issue in Compiler Explorer: https://godbolt.org/z/Kcr1dPvPz The static version: https://godbolt.org/z/WoqsKP9MY