https://gcc.gnu.org/bugzilla/show_bug.cgi?id=110905
Bug ID: 110905 Summary: GCC rejects constexpr code that may re-initialize union member Product: gcc Version: 13.2.1 Status: UNCONFIRMED Severity: normal Priority: P3 Component: c++ Assignee: unassigned at gcc dot gnu.org Reporter: danakj at orodu dot net Target Milestone: --- Godbolt: https://gcc.godbolt.org/z/v5anxqnP1 This repro contains a std::optional (which has a union) and it sets the union in a loop. Doing so causes GCC to reject the code as not being a constant expression. The error I was getting in my project was far more descriptive, with it trying to call the deleted constructor of the union. error: use of deleted function ‘sus::option::__private::Storage<sus::containers::VecIntoIter<sus::num::i32>, false>::<unnamed union>::<constructor>()’ In my more minimal test case the error is more terse and less clear. <source>:62:59: error: non-constant condition for static assertion 62 | static_assert(Flatten<int>({{1, 2, 3}, {}, {4, 5}}).sum() == 1 + 2 + 3 + 4 + 5); | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^~~~~~~~~~~~~~~~~~~~ <source>:62:51: error: '(((const std::vector<int, std::allocator<int> >*)(&<anonymous>)) != 0)' is not a constant expression 62 | static_assert(Flatten<int>({{1, 2, 3}, {}, {4, 5}}).sum() == 1 + 2 + 3 + 4 + 5); | ```cpp #include <optional> #include <vector> template <class T> struct VectorIter { constexpr std::optional<T> next() { if (front == back) return std::optional<T>(); T& item = v[front]; front += 1u; return std::optional<T>(std::move(item)); } constexpr VectorIter(std::vector<T> v2) : v(std::move(v2)), front(0u), back(v.size()) {} VectorIter(VectorIter&&) = default; VectorIter& operator=(VectorIter&&) = default; std::vector<T> v; size_t front; size_t back; }; template <class T> struct Flatten { constexpr Flatten(std::vector<std::vector<T>> v) : vec(std::move(v)) {} constexpr std::optional<T> next() { std::optional<T> out; while (true) { // Take an item off front_iter_ if possible. if (front_iter_.has_value()) { out = front_iter_.value().next(); if (out.has_value()) return out; front_iter_ = std::nullopt; } // Otherwise grab the next vector into front_iter_. if (!vec.empty()) { std::vector<T> v = std::move(vec[0]); vec.erase(vec.begin()); front_iter_.emplace([](auto&& iter) { return VectorIter<T>(std::move(iter)); }(std::move(v))); } if (!front_iter_.has_value()) break; } return out; } constexpr T sum() && { T out = T(); while (true) { std::optional<T> i = next(); if (!i.has_value()) break; out += *i; } return out; } std::vector<std::vector<T>> vec; std::optional<VectorIter<T>> front_iter_; }; static_assert(Flatten<int>({{1, 2, 3}, {}, {4, 5}}).sum() == 1 + 2 + 3 + 4 + 5); int main() {} ``` When the Flatten::next() method is simplified a bit, so that it can see the union is only initialized once, the GCC compiler no longer rejects the code. https://gcc.godbolt.org/z/szfGsdxb7 ```cpp #include <optional> #include <vector> template <class T> struct VectorIter { constexpr std::optional<T> next() { if (front == back) return std::optional<T>(); T& item = v[front]; front += 1u; return std::optional<T>(std::move(item)); } constexpr VectorIter(std::vector<T> v2) : v(std::move(v2)), front(0u), back(v.size()) {} VectorIter(VectorIter&&) = default; VectorIter& operator=(VectorIter&&) = default; std::vector<T> v; size_t front; size_t back; }; template <class T> struct Flatten { constexpr Flatten(std::vector<T> v) : vec(std::move(v)) {} constexpr std::optional<T> next() { std::optional<T> out; while (true) { // Take an item off front_iter_ if possible. if (front_iter_.has_value()) { out = front_iter_.value().next(); if (out.has_value()) return out; front_iter_ = std::nullopt; } // Otherwise grab the next vector into front_iter_. if (!moved) { std::vector<T> v = std::move(vec); moved = true; front_iter_.emplace([](auto&& iter) { return VectorIter<T>(std::move(iter)); }(std::move(v))); } if (!front_iter_.has_value()) break; } return out; } constexpr T sum() && { T out = T(); while (true) { std::optional<T> i = next(); if (!i.has_value()) break; out += *i; } return out; } bool moved = false; std::vector<T> vec; std::optional<VectorIter<T>> front_iter_; }; static_assert(Flatten<int>({1, 2, 3}).sum() == 1 + 2 + 3); int main() {} ``` Yet in the first example, the GCC compiler still rejects the code if only a single vector is passed in, so that the union is only initialized once, in the same way as the 2nd example: ``` static_assert(Flatten<int>({{1, 2, 3}}).sum() == 1 + 2 + 3); ```