关于对齐存储和琐碎的 copyable/destructible 类型

About aligned storage and trivially copyable/destructible types

我和一个比我聪明的人进行了一次有趣的讨论,我仍然有一个关于对齐存储和琐碎的 copyable/destructible 类型的悬而未决的问题。

考虑以下示例:

#include <type_traits>
#include <vector>
#include <cassert>

struct type {
    using storage_type = std::aligned_storage_t<sizeof(void *), alignof(void *)>;
    using fn_type = int(storage_type &);

    template<typename T>
    static int proto(storage_type &storage) {
        static_assert(std::is_trivially_copyable_v<T>);
        static_assert(std::is_trivially_destructible_v<T>);
        return *reinterpret_cast<T *>(&storage);
    }

    std::aligned_storage_t<sizeof(void *), alignof(void *)> storage;
    fn_type *fn;
    bool weak;
};

int main() {
    static_assert(std::is_trivially_copyable_v<type>);
    static_assert(std::is_trivially_destructible_v<type>);

    std::vector<type> vec;

    type t1;
    new (&t1.storage) char{'c'};
    t1.fn = &type::proto<char>;
    t1.weak = true;
    vec.push_back(t1);

    type t2;
    new (&t2.storage) int{42};
    t2.fn = &type::proto<int>;
    t2.weak = false;
    vec.push_back(t2);

    vec.erase(std::remove_if(vec.begin(), vec.end(), [](const auto &t) { return t.weak; }), vec.end());

    assert(vec.size() == 1);
    assert(!vec[0].weak);
    assert(vec[0].fn(vec[0].storage) == 42);
}

这是真实案例的简化版本。我真的希望我没有犯错误或简化太多。

如您所见,想法是存在一个名为 type 的类型(您知道,命名事物很困难)具有三个数据成员:

为了创建 type 的新实例(参见 main 函数),我在存储区中放置了一个值(intchar)以及 fn.
中静态函数模板 proto 的正确特化 稍后,当我想 调用 fn 并获取它的整数值 returns 时,我会这样做:

int value = type_instance.fn(type_instance.storage);

到目前为止,还不错。尽管存在风险和容易出错的事实(但这是一个示例,实际用例不是),这个有效
请注意,type 和我放入存储中的所有类型(示例中的 intchar)都需要可平凡复制和平凡破坏。这也是我讨论的核心。

问题(或者更好,疑问)出现在我将类型实例放入向量时(参见main 函数) 并决定从数组中删除其中一个,以便移动其他一些以保持它的打包。
更一般地说,我不再确定当我想复制或移动 type 的实例时会发生什么,以及它是否是 UB。

我的猜测是允许存储在存储中的类型可以简单地复制和简单地破坏。另一方面,有人告诉我这不是标准直接允许的,它可以被认为是 良性 UB,因为事实上几乎所有的编译器都允许你这样做那(我可以保证,对于 work 的某些定义,它似乎到处都是 work)。

所以,问题是:这是允许的还是 UB?在第二种情况下,我该怎么做才能解决这个问题?此外,C++20 会为此做出改变吗?

这个问题基本上减少到 建议的问题:

alignas(int) unsigned char buff1[sizeof(int)];
alignas(int) unsigned char buff2[sizeof(int)];

new (buff1) int {42};
std::memcpy(buff2, buff1, sizeof(buff1));

assert(*std::launder(reinterpret_cast<int*>(buff2)) == 42); // is it ok?

换句话说 - 当我四处复制字节时,我是否也四处复制 "object-ness"? buff1 肯定为 int 提供存储空间 - 当我们复制这些字节时,buff2 现在是否也为 int 提供存储空间?

答案是……不。根据 [intro.object]:

,正好 four ways 可以创建一个对象

An object is created by a definition, by a new-expression ([expr.new]), when implicitly changing the active member of a union, or when a temporary object is created ([conv.rval], [class.temporary]).

None 这些事情发生在这里,所以我们在 buff2 中没有任何类型的对象(在 unsigned char 的普通数组之外),因此行为未定义。简单地说,memcpy 不创建对象。

在原始示例中,只有第 3 行需要隐式对象创建:

assert(vec.size() == 1); // ok
assert(!vec[0].weak);    // ok
assert(vec[0].fn(vec[0].storage) == 42); // UB

这就是为什么 P0593 存在并且有一个特殊部分用于 memmove/memcpy:

A call to memmove behaves as if it

  • copies the source storage to a temporary area
  • implicitly creates objects in the destination storage, and then
  • copies the temporary storage to the destination storage.

This permits memmove to preserve the types of trivially-copyable objects, or to be used to reinterpret a byte representation of one object as that of another object.

这就是您在这里需要的 - 目前 C++ 目前缺少隐式对象创建步骤。


也就是说,您可以或多或少地依赖此 "doing the right thing",因为当今存在的大量 C++ 代码依赖于此代码 "just work."