关于对齐的存储和简单的可复制/可破坏类型

sky*_*ack 5 c++ undefined-behavior language-lawyer c++17

我与一个比我更聪明的人进行了一次有趣的讨论,我仍然提出了一个关于对齐存储和简单可复制/可破坏类型的公开问题.

请考虑以下示例:

#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);
}
Run Code Online (Sandbox Code Playgroud)

这是现实世界案例的简化版本.我真的希望我没有犯错或简化太多.

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

  • storage 这是一堆有大小的字节 sizeof(void *)
  • fn 指向具有类型的函数的指针 int(storage_type &)
  • weak 一个无用的bool只用于介绍这个例子

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

int value = type_instance.fn(type_instance.storage);
Run Code Online (Sandbox Code Playgroud)

到现在为止还挺好.尽管存在风险和容易出错的事实(但这是一个例子,真实的用例不是),这是有效的.
请注意,type我放入存储(int以及char示例中)的所有类型都必须是可以轻易复制的,并且可以轻易地进行破坏.这也是我讨论的核心.

问题(或更好的怀疑)出现时,我把例如在一个向量(见类型的实例main函数),并决定从阵列中删除其中的一个,所以,一些人正在四处走动,保持包装.
更一般地说,我不再确定当我想复制或移动实例时会发生什么type,如果它是UB的话.

我的猜测是允许存放在存储中的类型可以轻易地复制并且可以轻易地破坏.另一方面,我被告知这不是标准直接允许的,它可以被认为是一个良性的UB,因为几乎所有的编译器实际上允许你这样做(我可以保证这一点,它似乎是工作处处为的一些定义工作).

所以,问题是:这是允许的还是UB,我可以做些什么来解决第二种情况下的问题?而且,C++ 20是否会为此改变一切?

Bar*_*rry 6

这个问题基本上减少了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?
Run Code Online (Sandbox Code Playgroud)

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

答案是......不.根据[intro.object],有四种创建对象的方法:

当隐式更改联合的活动成员或创建临时对象时([conv.rval],[class.temporary]),对象由定义,new-expression([expr.new])创建. ).

这些事情都没有发生在这里,所以我们没有buff2任何类型的对象(仅在正常数组之外unsigned char),因此行为是未定义的.简单地说,memcpy不会创建对象.

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

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

这就是为什么P0593存在并且具有memmove/ 的特殊部分memcpy:

对memmove的调用就好像它一样

  • 将源存储复制到临时区域
  • 隐式地在目标存储中创建对象,然后
  • 将临时存储复制到目标存储.

这允许memmove保留简单可复制对象的类型,或者用于将一个对象的字节表示重新解释为另一个对象的字节表示.

这就是你需要的 - 今天C++中目前缺少隐式对象创建步骤.


也就是说,你可以或多或少地依赖于这种"做正确的事情",因为现在存在的庞大的C++代码依赖于这些代码"正常工作".