c++ - 关于对齐存储和普通可复制/可破坏类型

标签 c++ language-lawyer c++17 undefined-behavior

我和一个比我聪明的人进行了一次有趣的讨论,我仍然有一个关于对齐存储和普通可复制/可破坏类型的悬而未决的问题。

考虑以下示例:

#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 的类型(您知道,命名事物很困难)具有三个数据成员:

  • storage 是一堆大小为 sizeof(void *)
  • 的字节
  • fn 指向类型为 int(storage_type &)
  • 的函数的指针
  • weak 一个无用的 bool 值,仅用于介绍示例

为了创建 type 的新实例(参见 main 函数),我输入了一个值(int char) 在存储区和fn 中的静态函数模板proto 的正确特化。
稍后,当我想调用 fn 并获取它返回的整数值时,我会这样做:

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

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

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

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

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

最佳答案

这个问题基本上减少到什么LanguageLawyer建议:

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?

换句话说,当我四处复制字节时,我是否也四处复制“对象特性”? buff1确实为 int 提供存储空间- 当我们复制这些字节时,buff2现在还为 int 提供存储空间?

答案是……不。正好有 four ways根据 [intro.object] 创建对象:

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]).

这些事情都没有发生在这里,所以我们在 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++ 目前缺少隐式对象创建步骤。


也就是说,您可以或多或少地依赖这种“做正确的事”,因为当今存在的大量 C++ 代码都依赖于这种代码“正常工作”。

关于c++ - 关于对齐存储和普通可复制/可破坏类型,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/54512451/

相关文章:

c++ - 我可以在现代 Intel Core CPU 上测量分支预测失败吗?

c++ - 无法理解此 boost 条件变量示例

c++ - OpenGL 渲染与自己的 Phong 照明实现

c++ - 什么时候 `this` 是右值?

c++ - 不能在此上下文中应用属性 'fallthrough'

c++ - 如何编写类型特征来检查类型是否可以通过非缩小转换转换为另一种类型?

c++ - 根据其成员子对象之一的地址计算对象的地址

c++ - 普通查找和隐藏

c++ - 无法编译包含 "if constexpr"的函数模板实例化

c++ - 使用 "using declaration"扩展非类型模板参数包(模板可变参数编译时 SignalSlot 实现)