2

标准对下面的代码有什么看法?

#include <iostream>
#include <chrono>
#include <thread>

int main()
{
    static_assert(sizeof(int) == sizeof(float));
    using namespace std::chrono_literals;

    auto pi = new int{10};

    std::thread t([pi]() {
        for (int i = 0; i < 3; i++) {
            std::cout << *pi << '\n';
            std::this_thread::sleep_for(1s);
        }
    });

    std::this_thread::sleep_for(1s);
    auto pf = ::new (pi) float{};

    *pf = -1.0;

    t.join();
}

我特别好奇如何(或者,是否有可能)应用[basic.life]/7[basic.life]/8

类似地,在对象的生命周期开始之前但在对象将占用的存储空间已分配之后,或者在对象的生命周期结束之后并且在对象占用的存储空间被重用或释放之前

如果对象的生命周期结束之后,在对象占用的存储空间被重用或释放之前,在原对象占用的存储位置创建一个新对象

分别,鉴于[basic.life]/11

在本节中,“之前”和“之后”指的是“<a href="https://timsong-cpp.github.io/cppwp/n4659/intro.multithread#def:happens_before" rel="nofollow noreferrer" >发生在之前”的关系。

这是否意味着如果线程没有“看到”int对象生命周期结束,它可以访问它,就好像它还活着一样?

起初我认为该程序存在数据竞争

如果程序的执行包含两个潜在的并发冲突操作,则程序的执行包含数据竞争,其中至少一个不是原子的,并且两者都不会在另一个之前发生,除了下面描述的信号处理程序的特殊情况。任何此类数据竞争都会导致未定义的行为。

但是,没有相互冲突的动作。根据“冲突”的定义

如果其中一个修改内存位置而另一个读取或修改相同的内存位置,则两个表达式求值会发生冲突。

以及“内存位置”的定义

内存位置要么是标量类型的对象,要么是所有具有非零宽度的相邻位域的最大序列。

reads through*pi与 store through 不冲突*pf,因为这些左值表示不同的对象,因此表示不同的内存位置。

我觉得程序必须有UB,但看不到在哪里。

4

2 回答 2

1

[basic.life]/4本国际标准中赋予对象和引用的属性仅在其生命周期内适用于给定的对象或引用。

术语“期间”没有正式定义,但将其定义为“生命周期开始之后和生命周期结束之前”是合理的。

现在,happens-after的反义词不是happens-before,而是“ happens-before or is unsynchronized-with ”。因此,在对象生命周期开始之前发生不同步的操作,或者在对象生命周期结束之后发生不同步的操作,不会“在其生命周期内”执行。就这样的操作依赖于“归属于”该对象的“属性”而言,它表现出未定义的行为。

基于这些理由,我相信您的示例表现出未定义的行为,因为对对象的访问与其生命周期的结束*pi不同步的。

于 2020-01-29T04:35:16.077 回答
0

*pi在计算表达式后在线程中访问::new (pi) float{};会导致未定义的行为。

在同[basic.expr]一部分,第 5 段说:

程序可以通过重用对象占用的存储空间来结束任何对象的生命周期

一旦为int指向 by分配的存储空间pi被重新用于存储浮点数,对象的生命周期*pi就结束了。第 7 段说,当线程访问该值时,您有未定义的行为。

第 6 段中给出了一个示例,该示例与您正在做的事情相同,但没有线程,并评论说这是未定义的行为。

所有这一切的常见结果包括线程中的循环可能会打印相同的值 3 次(如果它*pi在循环之前读取一次并显示该值),或者您可能会得到10一次或两次,那么由-1.0f 的二进制值。但这些并不是唯一可能的结果。

于 2020-01-29T03:07:23.773 回答