代码之家  ›  专栏  ›  技术社区  ›  463035818_is_not_an_ai

重新解释强制转换与严格别名

  •  7
  • 463035818_is_not_an_ai  · 技术社区  · 6 年前

    post 我发现集中在C上,所以如果你能告诉我这是允许的,并且自从C++ 98/11……发生了什么变化,那就太好了。

    #include <iostream>
    #include <cstring>
    
    template <typename T> T transform(T t);
    
    struct my_buffer {
        char data[128];
        unsigned pos;
        my_buffer() : pos(0) {}
        void rewind() { pos = 0; }    
        template <typename T> void push_via_pointer_cast(const T& t) {
            *reinterpret_cast<T*>(&data[pos]) = transform(t);
            pos += sizeof(T);
        }
        template <typename T> void pop_via_pointer_cast(T& t) {
            t = transform( *reinterpret_cast<T*>(&data[pos]) );
            pos += sizeof(T);
        }            
    };    
    // actually do some real transformation here (and actually also needs an inverse)
    // ie this restricts allowed types for T
    template<> int transform<int>(int x) { return x; }
    template<> double transform<double>(double x) { return x; }
    
    int main() {
        my_buffer b;
        b.push_via_pointer_cast(1);
        b.push_via_pointer_cast(2.0);
        b.rewind();
        int x;
        double y;
        b.pop_via_pointer_cast(x);
        b.pop_via_pointer_cast(y);
        std::cout << x << " " << y << '\n';
    }
    

    请不要太注意可能的越界访问,事实上,也许没有必要写这样的东西。我知道 char* 可以指任何东西,但我也有 T* 指的是 字符* . 也许我还缺了点什么。

    这是一个 complete example memcpy ,而afaik不受严格别名的影响。

    3 回复  |  直到 6 年前
        1
  •  7
  •   user743382 user743382    6 年前

    我知道 char* T* 指的是 字符* .

    对,这是个问题。指针强制转换本身定义了行为,使用它访问类型为的不存在的对象 T

    与C不同,C++不允许即兴创作对象。 * . 不能简单地将某个内存位置指定为类型 T型 如果要创建该类型的对象,则需要该类型的对象已经存在。这需要安置 new

    1[…]安 由定义(6.1)创建,由 新表达式 (8.3.4),隐式更改工会的活跃成员(12.3),或创建临时对象(7.4,15.2)。[...]

    既然你没有做这些事情,就不会创建任何对象。

    此外,C++不隐含地考虑指向相同对象的不同对象的指针。你的 &data[pos] 计算指向 char 反对。把它扔到 T型* 没有指出任何 T型 位于该地址的对象,并且取消对该指针的引用具有未定义的行为。C++ 17添加 std::launder ,这是一种让编译器知道您要访问该地址上的不同对象,而不是您所指向的对象的方法。

    当您修改代码以使用布局时 新的 标准:洗涤槽 ,并确保您没有未对齐的访问(为简洁起见,我假设您省略了这一点),您的代码将具有定义的行为。

    *

        2
  •  6
  •   cHao Hammerite    6 年前

    别名是两个引用同一对象时出现的情况。可能是引用或指针。

    int x;
    int* p = &x;
    int& r = x;
    // aliases: x, r и *p  refer to same object.
    

    int foo(int* a, int* b) {
      *a = 0;
      *b = 1;
      return *a; 
      // *a might be 0, might be 1, if b points at same object. 
      // Compiler can't short-circuit this to "return 0;"
    }
    

    如果指针是不相关的类型,编译器就没有理由期望它们指向同一个地址。这是最简单的UB:

    int foo( float *f, int *i ) { 
        *i = 1;               
        *f = 0.f;            
       return *i;
    }
    
    int main() {
        int a = 0;
    
        std::cout << a << std::endl; 
        x = foo(reinterpret_cast<float*>(&a), &a);
        std::cout << a << "\n";   // Surprise? 
    }
    

    简单地说,严格的别名意味着编译器希望不相关类型的名称引用不同类型的对象,从而位于不同的存储单元中。因为用于访问这些存储单元的地址事实上是相同的,所以访问存储值的结果是未定义的,通常取决于优化标志。

    memcpy() 通过获取地址、指向char的指针以及在库函数的代码中存储数据的副本来避免这种情况。

    严格的别名适用于工会成员,这是单独描述的,但原因是相同的:给工会的一个成员写信并不能保证其他成员的值发生变化。这不适用于存储在union中的struct开头的共享字段。因此,禁止联合使用类型双关语。(由于历史原因和维护遗留代码的便利性,大多数编译器都不尊重这一点。)

    自2017标准起: 6.10左值和右值

    8如果程序试图访问对象的存储值 行为未定义

    (8.2)对象动态类型的cv限定版本,

    (8.3)与 对象,

    (8.4)一种有符号或无符号类型,对应于 对象的动态类型,

    (8.5)与 对象的动态类型的cv限定版本,

    (8.6)包括 (递归地包括

    (8.7)一种(可能是cv合格的)基类类型 对象的动态类型,

    (8.8)字符、无符号字符或std::字节类型。

    在7.5中

    T型的cv分解是一个cvi和Pi的序列,使得T是cv0 P0 cv1 P1···cvn1pn1cvnu,对于n>0,其中 cvi是一组cv限定符(6.9.3),每个Pi都指向 (11.3.1),指向类型为(11.3.3)的Ci类成员的指针,数组为 Ni,或(11.3.4)的未知界限数组。如果Pi指定 数组中,元素类型上的cv限定符cvi+1也取为 类型id const int**有两个cv分解,将U作为int和 作为指向const int.end example]的指针,cv限定符的n元组 在T的最长cv分解中的第一个之后,也就是说,

    2两种类型T1和T2相似,如果它们具有 相同的n使得相应的Pi分量相同 用U表示的类型是相同的。

    结果是:虽然可以重新解释将指针转换为不同的、不相关的和不相似的类型,但不能使用该指针访问存储值:

    char* pc = new char[100]{1,2,3,4,5,6,7,8,9,10}; // Note, initialized.
    int* pi = reinterpret_cast<int*>(pc);  // no problem.
    int i = *pi; // UB
    int* pc2 = reinterpret_cast<char*>(pi+2)); 
    char c = *pc2; // no problem, unless increment didn't put us beyond array bound.
    

    Reinterpret cast也不会创建它们指向的对象,并且将值赋给不存在的对象是UB,因此,如果cast指向的类不是微不足道的,也不能使用取消引用的结果来存储数据。

        3
  •  2
  •   Daniel Langr    6 年前

    简短回答:

    1. 您不能这样做: *reinterpret_cast<T*>(&data[pos]) = 直到有一个类型的对象 T 在指定的地址构造。你可以通过放置新的来完成。

    2. 即使这样,你可能需要使用 std::launder 至于C++ 17和以后,因为您访问了创建的对象(类型) T型 )通过指针 &data[pos] 类型 char* .

    “直接” reinterpret_cast T型 std::byte char ,或 unsigned char .

    在C++ 17之前,我将使用 memcpy -基于解决方案。编译器可能会优化掉任何不必要的副本。