在一个真实的项目中,我偶然发现了一些(一些版本的)编译器的奇怪行为。考虑以下类声明:
struct OptionalsStruct {
struct InnerType {
bool b{};
};
OptionalsStruct() = default;
std::optional<InnerType> oInnerType;
};
对于一些编译器,你有OptionalStruct::InnerType
是 *nothrow可构造的 *,但不是 constructible 或 *default可构造的 *(clang 11到16和GCC 10),对于其他一些编译器,它既不是 *nothrow可构造的 *(clang 9和10),更不用说clang 8是如何看待整个事情的。
我的问题是:这些行为是编译器的错误,还是标准中的漏洞(我使用的是C++17)?我错过了什么吗?
2条答案
按热度按时间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。)8yoxcaq72#
灵感来自this answer。
一个编译器有效地处理每个类在两个通道。成员函数体在第二个通道中处理,这就是为什么他们可以使用其他成员声明在他们下面(这已经在第一个通道中看到)。
显然,在
InnerType
上的第二次遍历直到OptionalsStruct
上的第一次遍历完成才开始。非静态数据成员初始化器也会在第二遍中处理,这包括
b
的{}
。这意味着InnerType
的可构造性在第一遍中是未知的。当
std::optional
在第一遍尝试查询可构造性时,编译器会报告垃圾值。它们会记住垃圾值,并在查询相同的trait时再次报告它们,即使这发生在OptionalsStruct
完全定义之后。修复方法是向
InnerType
添加一个非=default
的构造函数,以使其在第一次传递时就知道其可构造性。