C 中的类型问题

Type-punning issue in C++

本文关键字:问题 类型      更新时间:2023-10-16

我有一个模板类,带有bool作为模板参数Dynamic<bool>。无论参数是正确还是错误,它都具有完全相同的数据成员。他们的成员功能只是不同的。

我需要暂时将一个情况转换为另一个情况,而不是使用复制/移动构造函数。所以我求助于型号。为了确保这是一个问题,我使用了两个static_asserts

d_true=Dynamic<true>(...);
...
static_assert(sizeof(Dynamic<true>)==sizeof(Dynamic<false>),"Dynamic size mismatch");
static_assert(alignof(Dynamic<true>)==alignof(Dynamic<false>),"Dynamic align mismatch");
Dynamic<false>& d_false=*reinterpret_cast<Dynamic<false>*>(&d_true);
...

所以我认为我正在做的事情是安全的,如果有什么问题,编译器会给我一个static_assert错误。但是,GCC发出警告:

warning: dereferencing type-punned pointer will break strict-aliasing rules [-Wstrict-aliasing]

我的问题是双重的:我正在为实现这一目标做的最好方法吗?如果是这样,怎么能说服GCC安全并摆脱警告?

一种明显的可能性是将两个共有的数据分开到自己的类(或struct(中,然后在需要时从对象中获取。

struct Common {
// ...
};
template <bool b>
class Dynamic { 
    Common c;
public:
    Common &get_data() { return c; }
    // ...
};

从那里开始,其余的似乎很明显 - 当您需要Dynamic<whatever>的数据时,请致电get_data(),然后离开。

当然,关于一般主题有很多变化,例如,您可以使用继承:

struct Common { /* ... */ };
template <bool t>
class Dynamic : public Common {
    // ...
};

这消除了额外的 c.,以前的版本需要每次参考通用数据,但是(至少在我看来(继承可能太高了。

在标准中,"禁止"重新解释从类型A到类型的内存区域。这称为Aliasing。混音有3个例外,相同类型具有不同的CV资格,基本类型和char[]的区域。(对于char,贬损仅在char方向上单向式(

如果您使用std::aligned_storage和新的位置,则可以将该区域重新释放到所需的任何东西中,而无需编译器可以抱怨。这就是variant的工作方式。

编辑 :好的,上面的实际上是正确的(只要您忘记了std::launder(,但由于"生命周期"而误导。只有一个对象可以一次存在于存储空间的顶部。因此,通过另一种类型的视图来解释它是不确定的行为。 关键是构造。

如果我可能建议,请转到cppReference,以其static_vector示例,将其简化为1的情况1。添加一些Getters,恭喜,您已重新发明bitcast :)(提案http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2017/p0476r2.html(。

可能看起来像这样:

#include <type_traits>
#include <string>
#include <new>
#include <cstring>
#include <iostream>
using namespace std;
template< bool B >
struct Dynamic
{
    template <bool B2 = B>
    void ConditionalMethod(typename enable_if<B2>::type** = 0)
    {}
    string m_sharedObject = "stuff";
};
int main()
{
    using D0 = Dynamic<false>;
    using D1 = Dynamic<true>;
    aligned_storage<sizeof(D0), alignof(D0)>::type store[1];
    D0* inst0 = new (&store[0]) D0 ;
    // mutate
    inst0->m_sharedObject = "thing";
    // pune to D1
    D1* inst1 = std::launder(reinterpret_cast<D1*>(&store[0]));
    // observe aliasing
    cout << inst1->m_sharedObject;
    inst0->~D0();
}

请参见Wandbox中的生效

编辑:经过长时间的讨论之后,新标准中还有其他部分" 8.2.1.11",这更好地解释了为什么这不是严格有效的。我建议参考"生命周期"一章。
https://en.cppreference.com/w/cpp/language/lifetime
来自Miles Budnek评论:

该地址没有Dynamic<true>对象,通过Dynamic<false>访问它是未定义的行为。

在阅读了https://stackoverflow.com/a/57318684/2166857中的讨论之后解决我的问题。这仅在

时起作用

1(两种类型的对齐和大小匹配

2(两种类型都可以复制(https://en.cppreference.com/w/cpp/named_req/trivalycopyable(

首先使用aligned_storage

定义内存类型
typedef std::aligned_storage<sizeof(Dynamic<true>),alignof(Dynamic<true>)>::type DynamicMem;

然后引入该类型的变量

的变量
DynamicMem dynamic_buff;

然后使用新的位置将对象启动到主要类(在我的情况下,动态(

new (&dynamic_buff) Dynamic<true>();

然后,只要需要使用reinterpret_cast来定义对象引用或范围中与之关联的指针

{
    Dynamic<true>* dyn_ptr_true=reinterpret_cast<Dynamic<true>*>(&dynamic_buff)
    // do some stuff with dyn_ptr_true
}

这个解决方案绝不是完美的,但是它确实为我完成了工作。我确实鼓励大家阅读线程https://stackoverflow.com/a/57318684/2166857,然后在@miles_budnek和 @v.oddou之间来回遵循。我当然从中学到了很多。

标准C 中唯一的安全类型punning方法是通过std::bit_cast。但是,如果编译器无法优化,这可能涉及副本,而不是将相同的内存表示形式视为不同类型。此外,当前std::bit_cast仅由MSVC支持,尽管您可以使用__builtin_bit_cast

由于您使用的是GCC,因此可以使用属性__may_alias__来告诉它是安全的

template<int T>
struct Dynamic {};
template<>
struct Dynamic<true>
{
    uint32_t v;
} __attribute__((__may_alias__));
template<>
struct Dynamic<false>
{
    float v;
} __attribute__((__may_alias__));
float f(Dynamic<true>& d_true)
{
    auto& d_false = *reinterpret_cast<Dynamic<false>*>(&d_true);
    return d_false.v;
}

clang和ICC也支持该属性。请参阅Godbolt上的演示,没有任何警告