llvm / llvm-project

The LLVM Project is a collection of modular and reusable compiler and toolchain technologies.
http://llvm.org
Other
29.11k stars 12.01k forks source link

std::optional as a member variable of a parent class, cause std::is_constructible_v to fail. #106346

Open Marc-Pierre-Barbier opened 2 months ago

Marc-Pierre-Barbier commented 2 months ago

Hello,

while trying to migrate one of my apps to clang i discovered the following issue.

i have a class A contained within class B. class A has a std::optional<B> as a member value. class B contains a double that i want initialise to 0. the simples option is to use =0 or {} instead of writing a constructor. but when i do that combined with the std::optional<B> any usage of std::optional<B>::emplace(not limited to the member variable) will fail to compile. This is because B is no longer constructible according to is_constructible_v but it still according to is_default_constructible_v. The weirdest part ? commenting out std::optional<B> will allow the code to compile alternatively defining a default contructor in B that do the initialisation of the double will also fix it.

Tested with the following code using clang 17(using gcc10)

#include <optional>

class A {
public:
    class B {
        public:
        //removing the {} will fix the compilation
        double example{};
    };
    //alternatively commenting this will fix the compilation
    std::optional<B> transmission{};
};

int main(int argc, char ** argv) {
    static_assert(std::is_constructible<A::B>::value);
    static_assert(std::is_default_constructible_v<A::B>);

        //will fail
    static_assert(std::is_constructible_v<A::B>);

    std::optional<test> a;
    //emplace use std::is_constructible_v so it too will fail.
    a.emplace();
    return 0;
}
Marc-Pierre-Barbier commented 2 months ago

interestingly, if i copy the definition if is_constructible_v from gcc10 and 13 i get working functions.

namespace notstd {
template <typename _Tp, typename... _Args>
  inline constexpr bool is_constructible_v13 = __is_constructible(_Tp, _Args...);

template <typename _Tp, typename... _Args>
  inline constexpr bool is_constructible_v10 =
    std::is_constructible<_Tp, _Args...>::value;
}

still the import using stay broken

llvmbot commented 2 months ago

@llvm/issue-subscribers-clang-frontend

Author: Marc barbier (Marc-Pierre-Barbier)

Hello, while trying to migrate one of my apps to clang i discovered the following issue. i have a class A contained within class B. class A has a std::optional<B> as a member value. class B contains a double that i want initialise to 0. the simples option is to use =0 or {} instead of writing a constructor. but when i do that combined with the std::optional<B> any usage of std::optional<B>::emplace(not limited to the member variable) will fail to compile. This is because B is no longer constructible according to `is_constructible_v` but it still according to `is_default_constructible_v`. The weirdest part ? commenting out std::optional<B> will allow the code to compile alternatively defining a default contructor in B that do the initialisation of the double will also fix it. Tested with the following code using clang 17(using gcc10) ```C++ #include <optional> class A { public: class B { public: //removing the {} will fix the compilation double example{}; }; //alternatively commenting this will fix the compilation std::optional<B> transmission{}; }; int main(int argc, char ** argv) { static_assert(std::is_constructible<A::B>::value); static_assert(std::is_default_constructible_v<A::B>); //will fail static_assert(std::is_constructible_v<A::B>); std::optional<test> a; //emplace use std::is_constructible_v so it too will fail. a.emplace(); return 0; } ```
MitalAshok commented 2 months ago

Related: #36032 / CWG1351

The reason this happens is that optional instantiates is_constructible_v<A::B>. To check this, the compiler needs to instantiate the implicitly declared default constructor. The noexcept specifier of that depends on the initialiser, which has been delayed until a complete class context. Since it's not available, Clang reports false (perhaps it should error here instead?). Templates are cached, so is_constructible_v<A::B> remains false.

It has to be delayed because the member initializer in the nested class is a complete class context of the enclosing class as well (https://eel.is/c++draft/class.mem.general#note-5), so it can't be parsed until the enclosing class is actually complete:

struct X {
  struct Y {
    int mem = /* complete-class context of X, cannot parse before every member of `X` is parsed */ later_declared;
  };
  static constexpr int later_declared = 4;  // Available in the default member initializer
};

It's only the noexcept specifier calculation that's a problem: https://godbolt.org/z/q34c8hdx7

#include <optional>

class A {
public:
    class B {
        public:
        constexpr B() noexcept = default;  // Don't need the default initializer anymore
        double example{};
    };
    std::optional<B> transmission{};
};

int main(int argc, char ** argv) {
    static_assert(std::is_constructible<A::B>::value);
    static_assert(std::is_default_constructible_v<A::B>);

    static_assert(std::is_constructible_v<A::B>);

    std::optional<A::B> a;
    a.emplace();
    return 0;
}

Though this might be a Clang bug: the implicit exception specification might need to be delayed until a complete class context, like how explicit exception specifications are delayed. (So is_constructible_v would be true and is_nothrow_constructible_v would error exception specification is not available until end of class definition)