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);
```

Reply via email to