Why does clang, using libstdc++, delete the explicitly defaulted constructor on a type containing std::optional?
Asked Answered
U

1

17

Consider the following structure with an std::optional containing a type that definitely has a "normal" default constructor.

#include <optional>
#include <string>

struct Foo
{
    Foo() = default;
    const std::optional<std::string> m_value;
};

bool function()
{
    Foo foo;
    return bool(foo.m_value);
}

Compiling the following with clang 9 (using the system's default libstdc++, for its gcc 8) gives an unexpected warning:

<source>:6:5: warning: explicitly defaulted default constructor is implicitly deleted [-Wdefaulted-function-deleted]
    Foo() = default;
    ^
<source>:7:38: note: default constructor of 'Foo' is implicitly deleted because field 'm_value' of const-qualified type 'const std::optional<std::string>' (aka 'const optional<basic_string<char> >') would not be initialized
    const std::optional<std::string> m_value;
                                     ^

There's also hard error for Foo foo; since it uses said deleted constructor.

  • Deleting the Foo() = default; constructor gives the same results.
  • Replacing it with Foo() {} works!
  • Removing all the constructors and initializing foo as Foo foo{}; works!
  • Explicitly initializing the member as const std::optional<std::string> m_value{}; works!
  • Removing the const from the member works! (but isn't the same meaning)
  • Using clang 9 with -stdlib=libc++ works!
  • Using gcc 8.3 (still with libstdc++) works!

I've read std::optional - construct empty with {} or std::nullopt? which seems to indicate that the libstdc++ implementation choice of an = default constructor for std::optional is likely to blame. But, in that question, the concern was a matter of efficiency of one approach vs. the other. In this case, it seems like a matter of correctness.

(I suspect that the answer to How can std::chrono::duration::duration() be constexpr? is going to be part of the story here.)

I see the same behaviour on Compiler Explorer: https://gcc.godbolt.org/z/Yj1o5P

Compare simple structures of optional and non-optional std::string (in the non-working configurations):

struct Foo { const std::optional<std::string> m_value; };
auto f1() { Foo f; return f.m_value; } // Fails: call to implicitly deleted constructor.

struct Bar { const std::string m_value; };
auto f2() { Bar b; return b.m_value; } // Works.

Is this a bug in libstdc++? Is it mixed intentions and assumptions between clang and libstdc++?

Surely the intent can't be that I can have a structure with a const std::string but I can't have a structure with a const std::optional<std::string> unless I wrote a constructor?

(In real-word cases, you'd have additional constructors, too. Thus the motivation for an = default() constructor in the first place. That, and clang-tidy.)

Edit: Here's an expanded version of the example (Compiler Explorer) showing a similar example working in "pure clang", "pure gcc", but failing in mixed "clang+libstdc++". This slightly bigger example is still artificial, but hints at why one might want to actually have such a defaulted constructor.

struct Foo
{
    Foo() = default; // Implicitly deleted?!
    explicit Foo(std::string arg) : m_value{std::move(arg)} {}
    const auto& get() const noexcept { return m_value; }
  private:    
    const std::optional<std::string> m_value;
};

// Maybe return an empty or a full Foo.
auto function(bool flag, std::string x)
{
    Foo foo1;
    Foo foo2{x};
    return flag ? foo1 : foo2;
}
Unknown answered 27/8, 2020 at 20:52 Comment(6)
[dcl.init]/7 is likely the culprit.Ting
There's a compilation error with a const int member (godbolt), so the problem probably has to do with = deafult and const member variables.Stunner
std::optional has absolutely zilch to do with it. It is because of const.Heaviness
@IlCapitano: I am not surprised that const int value; deletes the default constructor (since int lacks a constructor). But seems like const optional<T> value; should work, no matter what T is, especially when const T value; works.Unknown
@n.'pronouns'm. Can you help me understand how it has nothing to do with optional when const string x works, but const optional<string> x doesn't work? And why does "pure clang+libc++" work and "pure gcc+libstdc++" work, but "mixed clang+libstdc++" not work?Unknown
It looks like a bug in gcc to me. see The library is a red herring, one has a =default default ctor and one has {}. I don't think the standard specifies it either way.Heaviness
M
3

This is a combination of:

  • an underspecification in the Standard;
  • a suboptimal Library implementation; and
  • a compiler bug.

Firstly, the Standard does not specify whether for the default optional.ctor a permissible implementation would be to defined it as defaulted:

constexpr optional() noexcept = default;
                              ^^^^^^^^^ OK?

Note that functions.within.classes answers the question in the affirmative for copy/move constructors, assignment operators, and non-virtual destructors, but does not mention default constructors.

This matters because it affects program correctness; on the assumption that optional has data members approximating the following:

template<class T>
class optional {
    alignas(T) byte buf[sizeof(T)]; // no NSDMI
    bool engaged = false;
    // ...
};

then since buf is a direct non-variant non-static data member lacking a default member initializer, if the default constructor of optional is defined as defaulted and thus is not user-provided, optional is not const-default-constructible and so optional<A> const a; is ill-formed.

It is thus a bad idea for a Library to define the default constructor of optional as defaulted, not only for this reason but also because it makes a value-initialized optional<B> b{}; perform more work than necessary, since it must zero-initialize buf, as observed std::optional - construct empty with {} or std::nullopt? - see in particular this answer. libstdc++ is fixed in this commit, which will be included in the next release of gcc, presumptively gcc 11.

Finally, it is a bug in gcc that it allows a non-const-default-constructible type for a const non-static data member without default member initializer of a class type whose default constructor is defined as defaulted; clang is correct to reject it. A reduced testcase is:

struct S {
    S() = default;
    int const i;
};

The best workaround in your case would be to supply a NSDMI:

const std::optional<std::string> m_value = std::nullopt;
                                         ^^^^^^^^^^^^^^

or (though I prefer the former, as it gives better codegen under Clang/libstdc++):

const std::optional<std::string> m_value = {};
                                         ^^^^

You might also consider giving Foo a user-defined default constructor; this results in better codegen under gcc (not zeroing the buffer but only setting the engaged member to false) thanks to what is presumably a related compiler bug.

Mother answered 4/12, 2020 at 17:10 Comment(1)
Thank you for the thorough answer. That all makes sense, and I like your advice for workarounds.Unknown

© 2022 - 2024 — McMap. All rights reserved.