1

我想完全了解函数调用参数如何交错的具体规定。在我看来,这有很多含义。举个例子:

void mad(cow_string a, cow_string b);
cow_string s("moo");
cow_string s1 = s;
cow_string s2 = s;
mad(s1+="haha",s2+="hahaha");

Sutter在cow_stringGotW 上描述的 Copy-On-Write 字符串容器在哪里:http ://www.gotw.ca/gotw/045.htm

  1. s1+="haha"如果对和的评估s2+="hahaha"以非常精细的粒度进行交错,这是否意味着这会在 cow_strings 内部引用计数上创建竞争条件(取决于编译器)?

  2. 如果我尝试使用互斥锁来防止竞争条件,那甚至不会导致单线程程序中的自锁(这让我很头疼)。例如,S1 制作内部副本并获取互斥体以减少引用计数上下文切换S2 也制作内部副本并运行到互斥体和 bam 自锁。

  3. (仅当第一个为真时)如果我的团队的其他成员不是专家或不知道它是牛,是否有安全的方法使一个对象成为牛?

编辑:

为清楚起见,Herb Sutters 的示例动摇了我对表达方式不是很交错的图片:

// In some header file:
void f( T1*, T2* );

// In some implementation file:
f( new T1, new T2 );

这样做:

allocate memory for the T1
construct the T1
allocate memory for the T2
construct the T2
call f()

或这个:

allocate memory for the T1
allocate memory for the T2
construct the T1
construct the T2
call f()

在这里阅读:http: //flylib.com/books/en/3.259.1.55/1/

第二次编辑:我想我假设一个引用计数器更改函数cow_string被内联,这是一个愚蠢的假设。没有那个愚蠢的假设,我的问题并没有多大意义。不过感谢您的回答!

4

2 回答 2

3

如果您的问题改为:

void mad(cow_string & a, cow_string & b);
cow_string s("moo");
cow_string s1 = s;
cow_string s2 = s;
mad(s1+="haha",s2+="hahaha");

你有一个可能更有意义的问题。s1 +=如果编译器以某种方式交错执行(可能是通过投入额外的线程),则和之间的交互s2 +=可能会干扰。

但是,不,它不能。C++ 编译器不会抛出额外的线程,它们不会 1/2 执行一个方法并切换到执行另一个。 s1'scow_string::operator+=s2'scow_string::operator+=将执行到完成,然后另一个才会开始,并且只有在两个完成后才会mad被调用。

调用中子表达式的执行顺序mad留给编译器实现 - 但它们不能以某种方式在单个线程中交错,标准编译器不能抛出额外的线程。

Herb Sutter 试图指出子表达式不需要以从左到右的顺序或深度第一顺序发生。相反,它们可以在函数调用本身的规则框架内以任何顺序(包括交错)发生!

最后一点很关键。它不能违反基本调用机制或完整参数传递周期的评估顺序。

所以,如果我们决定上面的表达式有 4 个小操作:

A)"haha"被转换为临时cow_string的,将交给cow_string::operator+=
B) "hahaha"
C) 来自 A 的临时工将被交给S1::+=
D) 来自 B 的临时工将被交给S2::+=

没有无限的方法可以降低,而是:

A、B、C、D
A、B、D、C
A、C、B、D
B、D、A、C
B、A、C、D
B、A、D、C

就是这样。诸如此类的函数调用cow_string(const char*)是不可交错的。也不是 operator +=。这些是函数调用。在调用它们之前,必须对它们的参数进行全面评估。在外部上下文中的任何进一步评估可以恢复之前,调用必须完全完成。

这是一个实际上是模棱两可的例子:

int a = 5;
foo(a+=9*4, a+=13/2);

编译器可以选择以它喜欢的任何顺序将参数(以及参数中的子表达式)评估为 foo 的顺序。因此,当foo()收到它时,最终的结果是任何人的猜测(并且会因编译器而异)。


至于您将两次调用 new 作为函数参数的编辑示例。

foo(new T1, new T2);

因为可以在任一构造函数之前调用其中一个或两个news,并且因为它们可以抛出,所以您有可能发生内存泄漏。

如果编译器生成:

new T1  
new T2  
T1()  
T2()  

如果new T2抛出,则记忆T1丢失。没有该空间的所有者T1会释放它。

即使编译器确实调用了new T1, T1(), new T2throw's ,您也可能在此处出现内存泄漏,因为没有人拥有T1占用的空间 - 您可能会遇到其他问题,因为T1' 的构造函数已运行,但现在已被放弃。所以它产生的任何副作用都不会被撤销/管理/清理/等。

继续阅读赫伯萨特。他的 Exceptional C++ 和 More Exceptional C++ 非常出色,并且深入探讨了这些问题!

于 2011-12-08T14:41:46.613 回答
1

我不确定你的问题是什么。在对 的调用中没有写入任何字符串mad,因此写入时复制不会起作用。唯一的副本来自+操作符的临时结果和值参数mad(这些可以省略)。

关于线程,线程问题是写时复制失宠的原因之一:它仍然被 g++ 使用,但它的线程处理存在错误(只能在一些非常特殊的情况下触发)。一般来说,做一个线程安全的写时拷贝并不难,做一个高效的写时拷贝也不难,但将两者结合起来几乎是不可能的。(至少对于.的界面来说std::basic_string<>。有一个更合理的界面,它不会那么困难。)

线程安全的写时复制中的关键问题是使使用计数的更新原子化,并且如果字符串将其实现暴露给外部修改(就像这样做std::basic_string),确保隔离实现的决定(确保始终使用 1 的计数,以便来自外部的修改不会影响其他实例)是原子的,带有字符串被隔离的标记。(最后一点是 g++ 实现失败的地方:如果您尝试在一个线程中复制字符串,并operator[]在另一个线程中通过它访问它,并且初始使用计数为 1,那么您最终可能会得到两个实例共享实现的副本它被标记为孤立的——C++ 源代码中的注释称之为“孤立”。)

无论如何,给定您显示的代码:使用 , 的写时复制实现,cow_string并且将共享一个实现,使用计数为 3。表达式 和每个都会​​创建一个新的临时字符串(使用计数1,最初)。但我不确定你的问题是什么:你的代码从不修改任何字符串,所以唯一的问题是确保使用计数的更新是原子的。ss1s2s1 + "haha"s2 + "hahaha"

于 2011-12-08T14:30:44.970 回答