代码之家  ›  专栏  ›  技术社区  ›  skypjack

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

  •  5
  • skypjack  · 技术社区  · 7 年前

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

    考虑下面的例子:

    #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

    创建的新实例的步骤 类型 main 函数),我输入一个值(或 int 或者 char )在存储区域和静态函数模板的右侧专门化 proto 在里面 fn .
    以后,当我想 援引 得到它返回的整数值,我做如下操作:

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

    到目前为止,一切顺利。尽管存在风险和容易出错的事实(但这是一个示例,实际用例并非如此),但 .
    注意 类型 int 烧焦

    这个 (或者更好的是 怀疑 )当我将类型eg的实例放入向量中时出现(请参见 主要的 函数),并决定从阵列中移除其中一个,以便移动其他一些以保持其压缩。
    更一般地说,当我想要复制或移动的实例时,我不再确定会发生什么 类型 不管是不是UB。

    我的猜测是,它被允许是放在存储中的类型,可以简单地复制,也可以简单地破坏。另一方面,我被告知,这不是标准直接允许的,它可以被视为 ,因为事实上几乎所有的编译器都允许您这样做(我可以保证这一点,看起来是这样的) 到处都可以找到一些关于 工作

    所以,问题是:这是允许的还是不允许的?在第二种情况下,我能做些什么来解决这个问题?此外,C++20是否会因此而改变?

    1 回复  |  直到 7 年前
        1
  •  6
  •   Barry    7 年前

    这个问题基本上归结为什么 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]:

    通过定义、新表达式([expr.new])、隐式更改联合的活动成员或创建临时对象([conv.rval]、[class.temporary])创建对象。

    所有这些事情都没有在这里发生,所以我们这里没有物体 任何类型的(仅在 unsigned char memcpy 不创建对象。

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

    这就是为什么 P0593 存在并有一个用于 memmove / memcpy

    • 将源存储复制到临时区域
    • 将临时存储复制到目标存储。

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


    也就是说,你可以或多或少地依赖于“做正确的事情”,因为现在存在的C++代码的巨大的实体依赖于这个代码“只是工作”。