关于对齐存储和琐碎的 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
的类型(您知道,命名事物很困难)具有三个数据成员:
storage
那是一堆大小为 sizeof(void *)
的字节
fn
指向类型为 int(storage_type &)
的函数的指针
weak
一个没用的bool,只是用来介绍例子的
为了创建 type
的新实例(参见 main
函数),我在存储区中放置了一个值(int
或 char
)以及 fn
.
中静态函数模板 proto
的正确特化
稍后,当我想 调用 fn
并获取它的整数值 returns 时,我会这样做:
int value = type_instance.fn(type_instance.storage);
到目前为止,还不错。尽管存在风险和容易出错的事实(但这是一个示例,实际用例不是),这个有效。
请注意,type
和我放入存储中的所有类型(示例中的 int
和 char
)都需要可平凡复制和平凡破坏。这也是我讨论的核心。
问题(或者更好,疑问)出现在我将类型实例放入向量时(参见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."
我和一个比我聪明的人进行了一次有趣的讨论,我仍然有一个关于对齐存储和琐碎的 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
的类型(您知道,命名事物很困难)具有三个数据成员:
storage
那是一堆大小为sizeof(void *)
的字节
fn
指向类型为int(storage_type &)
的函数的指针
weak
一个没用的bool,只是用来介绍例子的
为了创建 type
的新实例(参见 main
函数),我在存储区中放置了一个值(int
或 char
)以及 fn
.
中静态函数模板 proto
的正确特化
稍后,当我想 调用 fn
并获取它的整数值 returns 时,我会这样做:
int value = type_instance.fn(type_instance.storage);
到目前为止,还不错。尽管存在风险和容易出错的事实(但这是一个示例,实际用例不是),这个有效。
请注意,type
和我放入存储中的所有类型(示例中的 int
和 char
)都需要可平凡复制和平凡破坏。这也是我讨论的核心。
问题(或者更好,疑问)出现在我将类型实例放入向量时(参见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."