c++ 可选的嵌套类和is_constructible之间的奇怪交互

imzjd6km  于 2023-03-25  发布在  其他
关注(0)|答案(2)|浏览(82)

在一个真实的项目中,我偶然发现了一些(一些版本的)编译器的奇怪行为。考虑以下类声明:

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

    OptionalsStruct() = default;

    std::optional<InnerType> oInnerType;
};

对于一些编译器,你有OptionalStruct::InnerType是 *nothrow可构造的 *,但不是 constructible 或 *default可构造的 *(clang 1116GCC 10),对于其他一些编译器,它既不是 *nothrow可构造的 *(clang 910),更不用说clang 8是如何看待整个事情的。
我的问题是:这些行为是编译器的错误,还是标准中的漏洞(我使用的是C++17)?我错过了什么吗?

jexiocij

jexiocij1#

编译器直到最外层封闭类的}之后才将Inner视为“完全完成”,因为完成类上下文规则要求编译器延迟在b的默认成员初始化器中查找,直到该点之后。但某些性质封闭类的特殊成员函数的(隐式)声明可以间接依赖于默认成员初始化器的属性。
使用这种解释,您将有未定义的行为,可以使用不完整的类型作为模板参数示例化std::optional
实际上,std::optional的示例化可能会导致您在main中使用的类型trait特化的示例化,这取决于std::optional的实现。如果是这样,则类型trait特化是用不完整的类型示例化的,这也会导致未定义的行为,实际上无法产生合理的结果。
因为类模板专门化只示例化一次(并且编译器可以缓存结果),所以以后对traits的使用依赖于之前隐式示例化的无意义结果。
据我所知,嵌套类的完整性没有很好地指定,并且不清楚编译器的解释是否是“正确的”,这是标准中的一个公开缺陷。
(And从一个非常迂腐的Angular 来看,无论如何它都是每个标准的UB,因为std::optional<InnerType>的示例化点将在引起隐式示例化的命名空间范围声明之前,即在struct OptionalsStruct {之前,其中InnerType肯定是不完整的。参见CWG issue 287。)

8yoxcaq7

8yoxcaq72#

灵感来自this answer
一个编译器有效地处理每个类在两个通道。成员函数体在第二个通道中处理,这就是为什么他们可以使用其他成员声明在他们下面(这已经在第一个通道中看到)。
显然,在InnerType上的第二次遍历直到OptionalsStruct上的第一次遍历完成才开始。
非静态数据成员初始化器也会在第二遍中处理,这包括b{}。这意味着InnerType的可构造性在第一遍中是未知的。
std::optional在第一遍尝试查询可构造性时,编译器会报告垃圾值。它们会记住垃圾值,并在查询相同的trait时再次报告它们,即使这发生在OptionalsStruct完全定义之后。
修复方法是向InnerType添加一个非=default的构造函数,以使其在第一次传递时就知道其可构造性。

相关问题