我发现这个问题很难准确地表达。我在我的一个大型项目中遇到了一个奇怪的情况,我需要使用按值捕获的 lambda。简而言之,我有类似以下内容(及其变体):
struct my_data_t
{
std::vector<double> d_data;
std::vector<int> i_data;
};
我最终操作这些数据结构的方式是通过 lambda。假设我有一个 lambda,它根据
std::pair
: 设置第 i 个元素
const auto my_lam = [&](const int i, const std::pair<double, int>& elem)
{
data.d_data[i] = elem.first;
data.i_data[i] = elem.second;
};
我可以清楚地执行以下操作:
my_data_t data{{1.0, 1.3, 1.9, 9.3}, {1, 4, 3, 5}};
const auto my_lam = [&](const int i, const std::pair<double, int> elem)
{
data.d_data[i] = elem.first;
data.i_data[i] = elem.second;
};
my_lam(1, {100.3, 15});
这工作得很好,当我打印向量的内容时,我看到了我设置的值。然而,由于硬件原因太复杂/涉及到这里,我严格禁止通过引用捕获
data
。这是绝对的极限。
这是不言而喻的
my_data_t data{{1.0, 1.3, 1.9, 9.3}, {1, 4, 3, 5}};
auto my_lam = [=](const int i, const std::pair<double, int> elem) mutable
{
data.d_data[i] = elem.first;
data.i_data[i] = elem.second;
};
my_lam(1, {100.3, 15});
由于显而易见的原因对我不起作用。
我的代码库中当前的解决方案是“类似视图”的实现,例如
template <typename data_t> struct vec_image_t
{
data_t* raw;
std::size_t c_size;
// indexing operators, etc
};
然后将
my_data_t
修改为类似
template <template <typename> container_t = std::vector> struct my_data_t
{
container_t<double> d_data;
container_t<int> i_data;
};
这种方法效果很好,但存在一个问题,我需要编写大量样板代码才能将
my_data_t<std::vector>
转换为 my_data_t<vec_image_t>
,这意味着这种方法不能很好地扩展(我有很多 my_data_t
的变体)。当const
加入其中时,这也是一个完全令人头痛的问题。
当我意识到我真正需要的只是一个永远不会调用其复制构造函数和析构函数的
my_data_t
(如第一个没有template template
的实现所示)时,我尝试想出一些解决方案来解决这个问题。我查看了 c++20 的 bit_cast
,这似乎是我想要的,但它要求强制转换类型可以简单地复制,并且在例如的情况下std::vector
,这个不满意。
我想出了自己的演员阵容,就像
bit_cast
一样,只是没有繁琐的要求:
template <typename thing_t>
requires(sizeof(thing_t) == sizeof(image_t<thing_t>))
image_t<thing_t> make_image(thing_t& thing)
{
image_t<thing_t> output;
std::memcpy(&output.raw[0], &thing, output.cpy_size);
return output;
}
image_t<thing_t>
看起来像
template <typename thing_t> struct image_t
{
constexpr static std::size_t cpy_size = sizeof(thing_t);
char raw[cpy_size];
thing_t* operator -> ()
{
return (thing_t*)(&raw[0]);
}
thing_t& operator * ()
{
return *(thing_t*)(&raw[0]);
}
// and const versions...
};
这非常适合我的小问题:
my_data_t data{{1.0, 1.3, 1.9, 9.3}, {1, 4, 3, 5}};
auto d_img = make_image(data);
auto my_lam = [=](const int i, const std::pair<double, int> elem) mutable
{
d_img->d_data[i] = elem.first;
d_img->i_data[i] = elem.second;
};
my_lam(1, {100.3, 15});
对于我更大的代码库,这个解决方案可以让我消除大量的复杂性,让我的用户的生活更轻松。它还有一个优点,即
image_t<thing_t>
的作用与 thing_t*
非常相似,这符合使用语义。我知道存储 image_t<thing_t>
很危险,但我认为没有比存储迭代器更危险的了。
我的问题是这是否是未定义的行为。我担心这是UB,原因很明显,但根据标准,在“类型别名”下:
每当尝试通过 AliasedType 类型的泛左值读取或修改 DynamicType 类型的对象的存储值时,除非满足以下条件之一,否则该行为是未定义的:
- ...
- AliasedType is ... char ...:这允许检查任何对象作为字节数组的对象表示形式。
这似乎免除了我对 UB 的
make_image
演员阵容。是这样吗?
我查看了 c++20 的 bit_cast ,这似乎是我想要的,但它要求强制转换类型可以轻松复制,并且在例如以下情况下: std::vector,这个不满足。
存在此限制是因为在任何其他类型的对象之间复制对象表示(无论是通过
bit_cast
还是 memcpy
)都具有未定义的行为。
所以是的,UB。