【问题标题】:Can copy elision happen across synchronize-with statements?复制省略可以在 synchronize-with 语句中发生吗?
【发布时间】:2012-01-19 09:56:46
【问题描述】:

在下面的示例中,如果我们暂时忽略互斥体,复制省略可能会消除对复制构造函数的两次调用。

user_type foo()
{
  unique_lock lock( global_mutex );
  return user_type(...);
}

user_type result = foo();

现在复制省略的规则没有提到线程,但我想知道它是否真的应该跨越这样的界限。在上述情况下,逻辑抽象机器间线程中的最终副本发生在互斥锁释放之后。但是,如果省略副本,则结果数据结构会在互斥锁中初始化,因此它在互斥锁释放之前发生在线程间。

我还没有想到一个具体的例子,复制省略如何真正导致竞争条件,但内存序列中的干扰似乎是个问题。任何人都可以明确地说它不会导致问题,或者有人可以提出一个确实可以打破的例子吗?


为确保答案不仅仅针对特殊情况,请注意,如果我有类似new (&result)( foo() ) 的语句,仍然允许复制省略(根据我的阅读)。也就是说,result 不需要是堆栈对象。 user_type 本身也可以处理线程间共享的数据。


答案:我选择第一个答案作为最相关的讨论。基本上,由于标准说可能会发生省略,程序员只需要小心跨越同步边界发生。没有迹象表明这是有意还是无意的要求。我们仍然缺乏任何示例来说明可能出现的问题,因此无论哪种方式都可能不是问题。

【问题讨论】:

  • 我的大脑很难将function 作为函数名来阅读,纯粹是因为其他语言。所以我的编辑可能会有点争议;不过,希望你不要介意。好问题!
  • @LightnessRacesinOrbit 在您显然应该调用函数bar 的意义上存在争议?
  • “我还没有想到一个具体的例子,复制省略如何真正导致竞争条件”。我也是。问题是,在没有复制省略的情况下,在析构函数的两侧都调用了复制构造函数,因此无论有没有锁,调用复制 ctor 都必须“原则上”有效。为了想出一个坏的情况,我们需要它来关心被复制到/从对象的地址是什么。例如,我们可以将&result 传递给foo,并确保~unique_lock 使用result 的别名,以便我们的程序始终使用锁修改result
  • @EdA:你的意思是new (*result)(foo()) 可能吗?鉴于&result,除了您所谓的“堆栈对象”(尽管自动与静态存储持续时间的实际复杂性)之外,我看不出它怎么可能是任何东西
  • 我的意思不是同一个结果对象,只是全局空间某处的某个随机对象,或者从堆中分配。

标签: c++ c++11


【解决方案1】:

线程与它无关,但锁的构造函数/析构函数的顺序可能会影响你。

查看您的代码执行的低级步骤,没有复制省略,一一(使用 GCC 选项 -fno-elide-constructors):

  1. 构造lock
  2. 使用(...) 参数构造临时user_type
  3. 使用步骤 2 中的值复制并构造函数的临时返回值,类型为 user_type
  4. 从第 2 步销毁临时文件。
  5. 销毁lock
  6. 使用步骤 3 中的值复制构造 user_type result
  7. 从第 3 步销毁临时文件。
  8. 稍后,销毁result

自然,通过多次复制省略优化,它只是:

  1. 构造lock
  2. 直接用(...)构造result对象。
  3. 销毁lock
  4. 稍后,销毁result

请注意,在这两种情况下,带有(...)user_type 构造函数都受到锁的保护。任何其他复制构造函数或析构函数调用可能不受保护。

事后思考

我认为最有可能导致问题的地方是析构函数。也就是说,如果使用 (...) 构造的原始对象处理任何共享资源的方式与其副本不同,并且在析构函数中执行需要锁定的操作,那么您就有问题了。

当然,这意味着您的对象一开始就设计得很糟糕,因为副本的行为与原始对象不同。

参考

在 C++11 草案中,12.8.31(没有所有“移动”的类似措辞在 C++98 中:

当满足某些条件时,允许实现省略类的复制/移动构造 对象,即使对象的复制/移动构造函数和/或析构函数有副作用。在这种情况下, 该实现将省略的复制/移动操作的源和目标视为简单的两个不同 引用同一对象的方式,并且该对象的销毁发生在较晚的时间 当两个对象在没有优化的情况下被销毁时。这种复制/移动的省略 称为复制省略的操作在以下情况下是允许的(可以组合到 消除多个副本):

  • 在具有类返回类型的函数的 return 语句中,当表达式是 具有相同 cvunqualified 的非易失性自动对象(函数或 catch 子句参数除外) type 作为函数返回类型,可以通过构造省略复制/移动操作 自动对象直接转化为函数的返回值

  • 一个函数或 catch 子句参数),其范围不超出最内层的末尾 封闭的try-block(如果有的话),从操作数到异常的复制/移动操作 可以通过将自动对象直接构造到异常对象中来省略对象

  • 何时复制/移动尚未绑定到引用的临时类对象 对于具有相同 cv-unqualified 类型的类对象,可以通过以下方式省略复制/移动操作 将临时对象直接构造到省略的复制/移动的目标中

  • 当异常处理程序的异常声明声明了相同类型的对象时 (除了 cv-qualification)作为异常对象,如果程序的含义,可以通过将异常声明作为异常对象的别名来省略复制/移动操作 除了为声明的对象执行构造函数和析构函数外,将保持不变 异常声明。

第 1 点和第 3 点在您的示例中协作以省略所有副本。

【讨论】:

  • destroy lock 创建一个内存栅栏(应该是双向的)。直接构造为result 会更改该栅栏之前的变量,尽管逻辑代码仅在该栅栏之后修改该变量。这似乎违反了同步顺序。
  • 复制省略是否需要这样工作?我认为答案是肯定的:“在这种情况下,实现将省略的复制/移动操作的源和目标视为引用同一对象的两种不同方式”,这意味着通常的排序规则仍在起作用。
  • 内存栅栏与它无关。无论构造函数/析构函数实际做什么,C++ 语言都明确允许这种省略。如果这些省略号碰巧破坏了您的代码,那么您只是有一个错误。
  • @rodrigo,我愿意接受(基本上必须),但这是故意的特征吗?请注意,标准对其排序要求非常严格,允许省略简单地绕过这一点,没有太多注释,这似乎不寻常。
  • @edA-qamort-ora-y 这当然是一个不寻常的规则,因为它是 as-is 规则的一个例外,但它有据可查。请参阅我更新的答案中的参考。
【解决方案2】:

“为确保答案不仅仅针对特殊情况,请注意,如果我有类似new (&result)( foo() ) 的语句,仍然允许出现复制省略(根据我的阅读)。也就是说,结果不需要是一个堆栈对象。user_type 本身也可以处理线程之间共享的数据。"

问题是:如果result 是共享的,即使没有省略,您也会有数据竞争。该行为一开始就未定义。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2012-03-03
    • 1970-01-01
    • 2012-11-12
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多