可选未初始化的类:std::is_trivially_constructible 对于非默认构造函数似乎不正确?

Optionally-uninitialized class: std::is_trivially_constructible seems incorrect for non-default constructor?

提问人:Ben 提问时间:3/25/2021 更新时间:3/25/2021 访问量:303

问:

我觉得没有告诉我真相。一、语境:std::is_trivially_constructible<T, Arg>

我有一个小向量类,.默认构造它默认构造它的成员,所以 .这是所需的行为。VectorND<T, N>VectorND<float, 2>{} == VectorND<float, 2>{float{}, float{}} == VectorND<float, 2>{0.f, 0.f}

但是,有时,对于性能关键代码,我想在未初始化的情况下构造它们。我的想法是,我可以使用这样的标签类型:

struct uninitalized_t {};
static constexpr uninitalized_t uninitalized;
...
    VectorND(uninitalized_t) {}
...
VectorND<float, 2> x{uninitalized}; //< Tell x to be uninitialized.

我可以让它起作用。首先,如果我这样做

template <typename T, std::size_t N>
class VectorND {
    std::array<T, 2> x;
public:
    VectorND() = default;
    T& operator[](std::size_t i) { return x[i]; }
};

https://godbolt.org/z/oTvo33xEb

然后,默认构造函数使数据保持未初始化状态。与 if 相同。VectorND() {}

如果我做到了,那么数据就会归零,使其不再像预期的那样简单地构造。这是所需的默认行为。VectorND() : x{} {}

但是,如果我添加 ,那么看起来确实是未初始化的: https://godbolt.org/z/j9KWhPT7n 这又是我想要的。但出于某种原因,是.为什么?我试过了explicit VectorND(uninitalized_t) {}VectorND<float, 2> x{uninitialized}std::is_trivially_constructible_v<VectorND<float, 2>, uninitalized_t>false

VectorND() = default; // Or VectorND() {};
explicit VectorND(uninitalized_t) {}

VectorND() = default; // Or VectorND() {};
explicit VectorND(uninitalized_t) : VectorND() {}

我仍然得到.https://godbolt.org/z/3n1Mz9dWdstd::is_trivially_constructible_v<VectorND<float, 2>, uninitalized_t> == false

为什么?

C++ 初始化 类型特征

评论

0赞 ALX23z 3/25/2021
通常,通过它进行零初始化称为值初始化,与默认的未初始化构造不同。只有当构造设置为或根本不声明构造时,构造/分配才可能是微不足道的。VectorND x = {};VectorND x;= default
0赞 Ben 3/25/2021
所以即使不初始化数据,也不是微不足道的吗?(godbolt.org/z/ExoTdj34v 同意)。如果是这样的话,那么琐碎和初始化有什么区别呢?VectorND() {}
0赞 Human-Compiler 3/25/2021
标准中的AFAIK琐碎性只是默认构造函数,析构函数以及复制和移动构造函数/赋值运算符的一组标准。它与某些东西是否被初始化无关——只是与这些操作是否完全由编译器合成有关(例如,不是用户在任何地方定义的,但可能是编辑的)= default

答:

2赞 Human-Compiler 3/25/2021 #1

简答题

这是行不通的,因为这不是标准定义简单构造函数的方式。

琐碎与初始化(或缺乏初始化)无关,它只是标准提出的一组要求,用于将构造函数定义为琐碎的——一种状态,它使编译器能够更好地生成代码,并由库作者进行优化。

长答案

就 C++ 标准而言,琐碎的状态与成员是否初始化无关。琐碎的副产品是,您可能会从正在进行琐碎值初始化的对象中获取未初始化的数据,但这并不意味着未初始化的数据是琐碎的定义。

从形式上讲,该标准只是概述了非常具体的构造函数被视为微不足道的标准:

  • 默认构造函数由 class.default.ctor/3 定义

    如果默认构造函数不是用户提供的,并且如果:

    • 它的类没有虚函数 ([class.virtual]) 也没有虚拟基类 ([class.mi]),并且
    • 其类的非静态数据成员没有默认成员初始值设定项 ([class.mem]),并且
    • 其类的所有直接基类都具有简单的默认构造函数,并且
    • 对于其类中属于类类型(或其数组)的所有非静态数据成员,每个此类类都有一个简单的默认构造函数。

    否则,默认构造函数是非平凡的。

  • Copy/Move 构造函数由 class.copy.ctor/11 定义

    类 X 的复制/移动构造函数不是用户提供的,并且如果:

    • 类 X 没有虚函数 ([class.virtual]) 和虚拟基类 ([class.mi]),并且
    • 选择用于复制/移动每个直接基类子对象的构造函数是微不足道的,并且
    • 对于类类型(或其数组)的 X 的每个非静态数据成员,选择复制/移动该成员的构造函数是微不足道的;

    否则,复制/移动构造函数是非平凡的。

注意:对析构函数和析构函数也有类似的要求operator=

但是,对于不是复制、移动或默认构造函数的自定义构造函数,没有任何实际定义是微不足道的。这意味着任何自定义构造函数都不能被视为微不足道的。

这也有效地意味着,只有当它测试默认构造函数、复制构造函数或移动构造函数时,才能计算 to。std::is_trivially_constructible<T,Args...>::valuetrue

为什么琐碎很重要?

之所以存在“微不足道”的状态,是因为它为编译器和库作者提供了更好的优化保证。用于复制/移动的简单构造函数相当于简单的指令,只需复制数据(而不是需要任何清理或重新布线 对象的构造函数通常可能需要)。mov

此外,如果一个类型满足足够多的简单要求,它可能是“简单可复制的”——这允许编译器和库作者简单地复制数据,而不需要构造函数调用。这也可用于在不违反严格别名的情况下查看不同的对象表示形式。std::memcpy

评论

0赞 Ben 3/26/2021
感觉“微不足道”应该意味着“这个 c'tor 不执行任何动作”,所以没有回答这个问题让我很难过。就我想要的行为而言:一个默认为零但可以被告知构造为未初始化的类,我想出了如何做到这一点。如果我想问这个问题,我想我可以做到,然后想出一个通用的方法来构建一个。godbolt.org/z/qPPvzoxjjstd::is_trivially_constructible<T,Args...>::valuetemplate <typename T> concept is_uninitializable = std::is_trivial_v<T> || std::is_constructible_v<T, uninitalized_t>;
0赞 Human-Compiler 3/26/2021
@Ben 真正的问题是“编译器怎么知道它没有执行任何操作?这并不像看起来那么容易。构造函数可以在与声明不同的转换单元中定义,这意味着解析标头的编译器无法知道它不执行初始化(因为它看不到定义)。如果构造函数主体现在执行某些操作,但不进行初始化,这也会变得更加混乱。目前,构造函数的所有可查询属性都可以从声明中确定,而不是从定义中确定。
0赞 Ben 3/27/2021
啊,这是有道理的。我没有考虑过 c'tor 可能什么都不做,但不在标题中。我可以接受它要求任何定义都在标题中。