5

I have the following code to test my constexpr-constructible lazy class:

https://godbolt.org/z/rMLCiL

#include <optional>

template <class T>
class Lazy
{

    using initializer_t = T (*)();
    std::optional<T> m_val = std::nullopt;
    initializer_t m_initializer;

public:
    constexpr Lazy(initializer_t initializer = initializer_t{[] { return T{}; }}) noexcept
        : m_initializer{initializer} {}

    T& operator*()
    {
        if (!m_val.has_value()) {
            m_val = m_initializer();
        }
        return *m_val;
    }
    constexpr T* operator->() { return &(**this); }
};


#include <iostream>
struct A {
    int f() { return 10; }
    ~A()
    {
        std::cout << "Goodbye A " << (void*)this << std::endl;
    }
};
extern Lazy<A> a;

int val = a->f();

Lazy<A> a{[] { return A{}; }};

int main()
{
    std::cout << val << std::endl;
}

I expect it to print 10 in main. When compiled in clang-8.0, it runs as expected, but when compiled in gcc (either in 8.3 or in trunk), it causes a segmentation fault. It seems that a is not constant-initialized, and it's calling null a.m_initializer inside int val = a->f() before a gets initialized.

Cppreference says that std::optional<T> can be initialized to std::nullopt using a constexpr constructor, whether T is trivially-destructible or not. Thus, Lazy<A> a{[] { return A{}; }} should be constant-initialized before int val = a->f(); is initialized. If I comment out A::~A, it will run as expected even when compiled with gcc. Is this a bug in gcc, or am I missing something?

Update: I also found that if I make std::optional<T> a base class instead of having such member, it works correctly in gcc. Also, if I just change the line std::optional<T> m_val = std::nullopt; to std::optional<T> m_val;, it works correctly (std::optional<T> m_val{}; doesn't work). I don't really understand.

Jarod42
  • 203,559
  • 14
  • 181
  • 302
eivour
  • 1,678
  • 12
  • 20
  • This is somewhat unrelated to your question, but what stops you from doing a `template using Lazy = std::optional`. I take this approach all the time to define a lazy initialized var. – cplusplusrat Jul 28 '19 at 05:53
  • I didn't want to write lazy-construction `if(!a) a.emplace(...);` everytime I use `a`'s function. I wanted some fixed initialization (often with lengthy arguments) to be done when `a` is used the first time. I also often want to do some post-initialization on unmovable object (which I removed from the code above for simplicity.) – eivour Jul 28 '19 at 05:58
  • 1
    I really think it is a gcc bug, as using initializer list for `m_val` also fixes issue [Demo](https://godbolt.org/z/TEedJ3). – Jarod42 Jul 28 '19 at 21:53
  • I am not convinced this is a bug. I do not see how a compiler can be required to constant initialize variables with external storage as constant initialization needs to happen at compile time while the actual initialization of an external var may reside in a translation unit not visible at compile time. – cplusplusrat Jul 28 '19 at 22:59
  • Agreed. Here the extern var could const initialized. But is it *Required* to be? That is the heart of this issue. – cplusplusrat Jul 29 '19 at 17:09
  • @cplusplusrat In C++17, [an uncaptured lambda is constexpr](https://stackoverflow.com/questions/6420085/is-constexpr-supported-with-lambda-functions-expressions) and it is stated explicitly that [if the constructor, all member initializers, and all arguments are constexpr, an object is const-initialized](https://en.cppreference.com/w/cpp/language/constant_initialization). – eivour Jul 30 '19 at 14:29

1 Answers1

1

Is this a bug in gcc, or am I missing something?

Yes, one can verify that this bug (segmentation fault during program execution) was still reproducible till GCC 9.4, and the program runs nicely starting from GCC 10.1 (meaning that the bug was fixed). Demo: https://gcc.godbolt.org/z/osWa1no9Y

Fedor
  • 17,146
  • 13
  • 40
  • 131