Search code examples
c++c++17inner-classestype-traits

Weird interaction among optional, nested classes and is_constructible


Working on a real-life project, I've stumbled across the weird behavior of some (versions of some) compilers. Consider the following class declaration(s):

struct OptionalsStruct {
    struct InnerType {
        bool b{};
    };

    OptionalsStruct() = default;

    std::optional<InnerType> oInnerType;
};

For some compilers, you have that OptionalStruct::InnerType is nothrow constructible but not constructible or default constructible (clang 11 to 16 and GCC 10), for some other compilers it's neither nothrow constructible (clang 9 and 10), not to mention how clang 8 sees the whole thing.

My question is: are these behaviors compiler bugs, or even holes in the standard (I'm using C++17)? Am I missing something here?


Solution

  • The compilers don't consider Inner as "fully complete" until after the } of the outer-most enclosing class because the complete-class context rules require the compiler to delay lookup in the default member initializer for b until after that point, but certain properties of (implicit) declarations of special member functions of the enclosing class may indirectly depend on properties of the default member initializer.

    With that interpretation you have undefined behavior for instantiating std::optional with an incomplete type as template argument.

    Practically speaking, the instantiation of std::optional may cause instantiation of the type traits specializations you are using in main, depending on the std::optional implementation. If so, then the type trait specializations are instantiated with incomplete types, which also causes undefined behavior and practically can't produce sensible results.

    Because class template specializations are instantiated only once (and the compiler can cache the result), your later use of the traits then depends on the nonsensical results of the previous implicit instantiations.

    As far as I know it is an open defect in the standard that completeness of nested classes isn't well-specified and it is unclear whether the compiler's interpretation is the "correct" one.

    (And from a very pedantic point of view it is UB per standard anyway, because the point of instantiation of std::optional<InnerType> would be before the namespace scope declaration causing the implicit instantiation, i.e. before struct OptionalsStruct {, where InnerType is certainly incomplete. See CWG issue 287.)