这算是严格别名违规吗?任何类型的指针都能别名为 char 指针吗?

8

我仍在努力理解严格别名规则中允许和不允许的内容。通过这个具体示例,是否违反了严格别名规则?如果没有,为什么?是因为我将不同类型的placement new放置到char *缓冲区中吗?

template <typename T>
struct Foo
{
    struct ControlBlock { unsigned long long numReferences; };
    Foo()
    {
        char* buffer = new char[sizeof(T) + sizeof(ControlBlock)];
        // Construct control block
        new (buffer) ControlBlock{};
        // Construct the T after the control block
        this->ptr = buffer + sizeof(ControlBlock);
        new (this->ptr) T{};
    }
    char* ptr;

    T* get() { 
        // Here I cast the char* to T*.
        // Is this OK because T* can alias char* or because
        // I placement newed a T at char*
        return (T*)ptr;
    }
};

记载一下,void* 可以与任何类型指针别名,而且任何类型指针也可以与 void* 别名。一个 char* 可以与任何类型指针别名,但反过来是否成立呢?如果对齐正确,任何类型都可以别名为 char* 吗?所以下面这种写法是允许的吗?
char* buffer = (char*)malloc(16);
float* pFloat = buffer;
*pFloat = 6; // Can any type pointer alias a char pointer?
// If the above is illegal, then how about:
new (pFloat) float; // Placement new construct a float at pointer
*pFloat = 7; // What about now?

一旦我将char*缓冲区指针分配给新分配的内存,为了将其用作float缓冲区,我是否需要循环并在每个位置上使用placement new创建一个float?如果一开始没有将分配分配给char*,而是直接将其分配给float*,那么我就能立即将其用作float缓冲区,对吗?

2个回答

6

这是严格别名违规吗?

是的。

任何类型指针都可以别名为char指针吗?

不可以。

您可以对指针进行清洗:

T* get() { 
    return std::launder(reinterpret_cast<T*>(ptr)); // OK
}

或者,你可以存储放置新对象的结果:

Foo()
{
    ...
    this->ptr = new (buffer + sizeof(ControlBlock)) T{};
}
T* ptr;

T* get() { 
    return ptr; // OK
}

自从提案P0593R6被纳入语言(C++20)后,您就不需要循环并在每个位置放置新的浮点数了。在此之前,标准要求使用placement-new。您不必亲自编写该循环,因为标准库中有用于此目的的函数模板:std::uninitialized_fill_nuninitialized_default_construct_n等。此外,可以放心地使用优秀的优化器将这样的循环编译为零条指令。
constexpr std::size_t N = 4;
float* pFloat = static_cast<float*>(malloc(N * sizeof(float)));

// OK since P0593R6, C++20
pFloat[0] = 6;

// OK prior to P0593R6, C++20 (to the extent it can be OK)
std::uninitialized_default_construct_n(pFloat, N);
pFloat[0] = 7;

// don't forget
free(pFloat);

附注:在C++中不要使用std::malloc,除非你需要与需要它的C API进行交互(即使在C中这也是一个相当罕见的需求)。我还建议不要重复使用new char[]缓冲区,因为它对于演示目的来说是不必要的。相反,使用operator ::new来分配存储空间而不创建对象(即使是微不足道的对象)。或者更好的是,由于已经有了一个模板,让模板的使用者提供自己的分配器,以使您的模板更加通用。


6
严格别名(Strict aliasing)指的是,要想对一个 T* ptr 进行解引用操作,就必须存在一个 T 对象位于该地址上,并且它必须还活着。这意味着你不能简单地在两个不兼容的类型之间进行位移转换,并且编译器可以假定,指向不兼容类型的两个指针永远不可能指向同一个位置。
例外情况是指针类型为 unsigned char, char 或者 std::byte 时,你可以将任何对象指针重新解释为指向这三种类型之一的指针并对其进行解引用操作。 (T*)ptr; 是有效的,因为在 ptr 地址上存在一个 T 对象。这就是所有需要的,无论获取指针的方式是什么,通过多少次强制转换。当 T 具有常量成员时,还需要满足一些更多的要求,但这与放置 new 和对象复活有关 - 如果您感兴趣,可以查看此答案
对于第二个例子,可能即使没有常量成员,也会有影响,具体请参见相关问题和 @eerorika 的回答,建议使用 std::launder 或从放置 new 表达式中分配。

据记载,一个 void* 可以与其他任何类型的指针别名,而任何类型指针都可以将 void* 别名。

这并不是真的,void 不属于上述三种允许的类型。但我认为您只是误解了“别名”一词 - 严格别名仅适用于指针进行解引用操作时,当然,只要您不对它们进行解引用操作,就可以自由地拥有任意数量的指向任何位置的指针。由于无法对 void* 进行解引用操作,因此这是一个无意义的点。

回应您的第二个例子

char* buffer = (char*)malloc(16); //OK

// Assigning pointers is always defined the rules only say when
// it is safe to dereference such pointer.
// You are missing a cast here, pointer cannot be casted implicitly in C++, C produces a warning only.
float* pFloat = buffer; 
// -> float* pFloat =reinterpret_cast<float*>(buffer);

// NOT OK, there is no float at `buffer` - violates strict aliasing.
*pFloat = 6;
// Now there is a float
new (pFloat) float;
// Yes, now it is OK.
*pFloat = 7;

所以,如果只是关于存在哪个对象,那么我可以这样做:MyStruct1* p1 = malloc(16); MyStruct2* p2 = (MyStruct2*) p1; new (p2) MyStruct2{ }; 然后访问 p2。你提供的答案说,malloc返回的内存没有类型,因此我们使用任何指针来别名它。该内存的类型取决于您写入其中的内容。因此,如果您向malloc返回的缓冲区中写入一个浮点数,那么您无法将其与另一种类型别名。那么,如果我这样做 char* pBuffer = (char*)malloc(16); 为什么不能这样做 float* pFloat = (float*)pBuffer; 并在其中使用,如果该缓冲区中还没有任何类型? - Zebrafish
1
@斑马鱼,这是一种“哞点”(moo point),:)。 - Quimby
@Zebrafish,我可能在示例上有所错误,至少是关于从放置new中进行赋值的std::launder。有一些复杂的规则,可能会导致ptr指向一个“旧”的地址。请参考@eerorika的答案,并接受它,因为我认为在这方面它更好。 - Quimby
@Zebrafish 你所做的不是别名,别名必须涉及解引用。在放置 new 之前,你不能解引用 p1p2。 此外,正如 eerorika 所说,p2 应该从放置 new 中重新分配,例如 p2= new (p2) MyStruct2()。你可以有许多指向同一地址的不同指针,比如 int* p1=0;float* p2=0; T* p3=0;,这肯定不会违反任何规则,对吧? - Quimby
@Zebrafish 这有点复杂,我不是100%有信心。https://dev59.com/hV8d5IYBdhLWcg3wRAiE#27049038 实际上问的是同样的问题。 - Quimby
显示剩余6条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接