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

时间:2019-02-04 08:31:05

标签: 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一个无用的布尔值,仅用于介绍示例

要创建type的新实例(请参见main函数),我将一个值(intchar)放在存储区和proto中静态函数模板fn的右专业化。
稍后,当我想调用 fn并获取返回的整数值时,我会执行以下操作:

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

到目前为止,太好了。尽管存在风险和容易出错的事实(但这只是一个示例,但实际用例却并非如此),但这可行
请注意,type和我放入存储中的所有类型(在示例中为intchar)都必须是可复制和可破坏的。这也是我讨论的核心。

当我将类型的实例(例如,放在向量中)(请参见main函数)并决定执行以下操作时,就会出现问题(或更佳的是 doubt )从阵列中删除其中一个,以便其他一些移动以保持打包。
一般来说,我不再确定要复制或移动type的实例以及是否为UB时会发生什么。

我的猜测是,允许将存储类型中的类型轻松地复制和轻松地破坏。另一方面,有人告诉我该标准不允许直接使用,可以将其视为良性UB ,因为实际上几乎所有的编译器都允许您这样做(可以保证这一点,对于 work 的某些定义,似乎到处都是 work

因此,问题是:这是允许的还是UB?在第二种情况下,我该如何解决该问题?而且,C ++ 20是否会为此而改变?

1 个答案:

答案 0 :(得分: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?

换句话说-当我复制字节时,是否也复制“对象状态”? buff1当然可以为int提供存储-当我们复制这些字节时,buff2现在是否也为int提供存储了?

答案是……不。每个[intro.object]都有four ways个对象来创建对象:

  

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

这里没有发生任何事情,因此我们在buff2中没有任何类型的对象(除了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 ++当前缺少隐式对象创建步骤。


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