Do trivial destructors cause aliasing

Cor*_*ica 6 c++ memory destructor c++11

C++11 §3.8.1 declares that, for an object with a trivial destructor, I can end its lifespan by assigning to its storage. I am wondering if trivial destructors can prolong the object's lifespan and cause aliasing woes by "destroying an object" that I ended the lifespan of much earlier.

To start, something which I know is safe and alias-free

void* mem = malloc(sizeof(int));
int*  asInt = (int*)mem;
*asInt = 1; // the object '1' is now alive, trivial constructor + assignment
short*  asShort = (short*)mem;
*asShort = 2; // the object '1' ends its life, because I reassigned to its storage
              // the object '2' is now alive, trivial constructor + assignment
free(mem);    // the object '2' ends its life because its storage was released
Run Code Online (Sandbox Code Playgroud)

Now, for something which is not so clear:

{
    int asInt = 3; // the object '3' is now alive, trivial constructor + assignment
    short* asShort = (short*)&asInt; // just creating a pointer
    *asShort = 4; // the object '3' ends its life, because I reassigned to its storage
                  // the object '4' is now alive, trivial constructor + assignment
    // implicitly, asInt->~int() gets called here, as a trivial destructor
}   // 'the object '4' ends its life, because its storage was released
Run Code Online (Sandbox Code Playgroud)

§6.7.2 states that objects of automatic storage duration are destroyed at the end of the scope, indicating that the destructor gets called. If there is an int to destroy, *asShort = 2 is an aliasing violation because I am dereferencing a pointer of unrelated type. But if the integer's lifespan ended before *asShort = 2, then I am calling an int destructor on a short.

I see several competing sections regarding this:

§3.8.8 reads

If a program ends the lifetime of an object of type T with static (3.7.1), thread (3.7.2), or automatic (3.7.3) storage duration and if T has a non-trivial destructor,39 the program must ensure that an object of the original type occupies that same storage location when the implicit destructor call takes place; otherwise the behavior of the program is undefined.

The fact that they call out types T with non-trivial destructor as yielding undefined behavior seems, to me, to indicate that having a different type in that storage location with a trivial destructor is defined, but I couldn't find anywhere in the spec that defined that.

Such a definition would be easy if a trivial destructor was defined to be a noop, but there's remarkably little in the spec about them.

§6.7.3 indicates that goto's are allowed to jump into and out of scopes whose variables have trivial constructors and trivial destructors. This seems to suggest a pattern where trivial destructors are allowed to be skipped, but the earlier section from the spec on destroying objects at the end of the scope mentions none of this.

Finally, there's the sassy reading:

§3.8.1表明如果我的构造函数很简单,我可以随时启动对象的生命周期.这似乎表明我可以做类似的事情

{
    int asInt = 3;
    short* asShort = (short*)&asInt;
    *asShort = 4; // the object '4' is now alive, trivial constructor + assignment
    // I declare that an object in the storage of &asInt of type int is
    // created with an undefined value.  Doing so reuses the space of
    // the object '4', ending its life.

    // implicitly, asInt->~int() gets called here, as a trivial destructor
}
Run Code Online (Sandbox Code Playgroud)

这些读数中唯一似乎表明存在任何别名问题的是其自身的§6.7.2.看起来,当作为整个规范的一部分阅读时,平凡的析构函数不应以任何方式影响程序(尽管由于各种原因).有谁知道在这种情况下会发生什么?

Ste*_*sop 2

在你的第二个代码片段中:

{
    int asInt = 3; // the object '3' is now alive, trivial constructor + assignment
    short* asShort = (short*)&asInt; // just creating a pointer
    *asShort = 4; 
    // Violation of strict aliasing. Undefined behavior. End of.
}
Run Code Online (Sandbox Code Playgroud)

这同样适用于您的第一个代码片段。它不是“安全”的,但它通常会工作,因为(a)没有特殊原因实现编译器使其不起作用,并且(b)实际上编译器必须支持至少一些违反严格的别名,否则不可能使用编译器实现内存分配器。

据我所知,可以并且确实会促使编译器破坏此类代码,如果您asInt事后阅读,DFA 可以“检测”asInt未修改的代码(因为它仅通过严格别名违规(即 UB)进行修改) ,并将asInt写入后的初始化移至*asShort。不过,根据我们对标准的任何一种解释,这都是 UB——在我的解释中是因为严格的别名违规,在你的解释中asInt是因为在其生命周期结束后读取。所以我们都很高兴这不起作用。

不过我不同意你的解释。如果您认为分配给 的部分存储会asInt结束 的生命周期asInt,那么这与自动对象的生命周期是其范围的说法直接矛盾。好的,所以我们可以接受这是一般规则的一个例外。但这意味着以下内容无效:

{
    int asInt = 0;
    unsigned char *asChar = (unsigned char*)&asInt;
    *asChar = 0; // I've assigned the storage, so I've ended the lifetime, right?
    std::cout << asInt; // using an object after end of lifetime, undefined behavior!
}
Run Code Online (Sandbox Code Playgroud)

除了允许unsigned char作为别名类型(以及定义所有位 0 对于整数类型意味着“0”)的全部目的是使这样的代码工作。所以我非常不愿意对标准的任何部分进行解释,这意味着这是行不通的。

Ben 在下面的评论中给出了另一种解释,即*asShort赋值并不会结束asInt.

  • @CortAmmon:关于琐碎析构函数的实际问题的答案可能是它们不执行任何操作,特别是它们不需要在名义上执行时存在对象。这在 3.8.8 的引用中是明确的:它说非平凡的可破坏类型 *do* 需要一个对象,所以可以理解平凡的类型不需要。标准语有时很复杂,但我们可以假设它不是故意误导的:-) (3认同)