我想完全理解有关如何交错函数调用参数的确切内容。在我看来,有很多含义。请看以下示例:
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");
其中cow_string
是像Sutter在GotW上描述的写时复制字符串容器:http://www.gotw.ca/gotw/045.htm
如果s1+="haha"
和s2+="hahaha"
的评估被交错到非常精细的粒度,那么这并不意味着这会在cow_strings内部引用计数上创建竞争条件(取决于编译器) )?
如果我尝试使用互斥锁防止竞争条件,那么甚至不会导致单线程程序中的自锁(这让我的头部受伤)。例如。 S1进行内部复制并获取互斥锁以减少引用计数 上下文切换 S2也会生成内部副本并运行到互斥锁和bam自锁。
(只有在第一个为真的情况下)如果我的团队的其他成员不是大师或者不知道它是COW,是否有安全的方法让对象成为COW?
编辑:
为了清楚起见,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
中的引用计数器更改函数被内联,这是一个愚蠢的假设。没有这个愚蠢的假设我的问题并没有多大意义。谢谢你的答案!
答案 0 :(得分: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 ++编译器不会引入额外的线程,并且它们不会执行一个方法并切换到执行另一个方法。 s1
的{{1}}或cow_string::operator+=
的{{1}}将执行完成,只有这样才会开始,并且只有在s2
完成后才会{调用。
调用cow_string::operator+=
时子表达式的执行顺序留给了编译器实现 - 但它们不能以某种方式在单个线程中交错,而标准编译器不能引入额外的线程。
Herb Sutter正试图弄清楚子表达式不需要以从左到右的顺序发生,或者在第一顺序中发生。相反,它们可以在函数的规则框架内以任何顺序(包括交错)发生![/ p>
最后一件作品至关重要。它不能违反基本的调用机制,也不能违反完整的参数传递期的评估顺序。
所以,如果我们决定上面的表达式有4个迷你操作:
A)mad
转换为临时mad
,将转交"haha"
B)cow_string
的同样的事情
C)来自A的温度将交给cow_string::operator+=
D)来自B的温度将交给"hahaha"
没有无限的方法可以解决这个问题,而不是:
A,B,C,D
A,B,D,C
A,C,B,D
B,D,A,C
B,A,C,D
B,A,D,C
就是这样。诸如S1::+=
之类的函数调用不可交叉。运营商S2::+=
也不是。那些是函数调用。必须先对它们的参数进行全面评估才能调用它们。在外部环境中的任何进一步评估可能恢复之前,呼叫必须完全完成。
这是一个事实上含糊不清的例子:
cow_string(const char*)
编译器可以选择以什么顺序来评估foo的参数(以及参数中的子表达式),以满足它的任何顺序。那么当+=
收到它时最终会得到的是任何人的猜测(并且会因编译器而异)。
关于两个调用new作为函数参数的编辑示例。
int a = 5;
foo(a+=9*4, a+=13/2);
因为foo()
之一或两者都可以在构造函数之前调用,并且因为它们可以抛出,所以你可能会发生内存泄漏。
如果编译器生成:
foo(new T1, new T2);
如果new
抛出,则new T1
new T2
T1()
T2()
的内存将丢失。 new T2
空间的所有者不会解除分配。
即使编译器确实调用T1
,T1
,new T1
抛出,你也可以在这里发生内存泄漏,因为没有人拥有T1()
占用的空间 - 而你可能有其他问题,因为new T2
的构造函数已运行,但现在已被放弃。因此,它产生的任何副作用都不会被撤消/管理/清理/等等。
继续阅读Herb Sutter。他出色的C ++和更优秀的C ++非常出色,并深入探讨了这些问题!
答案 1 :(得分:1)
我不确定你的问题是什么。没有任何字符串的写入
在调用mad
时,写入时的复制不起作用。唯一的
副本来自+
运算符的临时结果和值
参数mad
(这些参数可以省略)。
关于线程,线程问题是其中一个原因
写作上的副本已经失宠了:它仍然被g ++使用,但有一个
它的线程处理错误(只能在某些情况下触发)
特殊情况下)。一般来说,制作一个并不难
写入时的线程安全副本,并不难做出有效的副本
在写,但几乎不可能将两者结合起来。 (至少对于
std::basic_string<>
的界面。有了更合理的界面,
这不会那么困难。)
线程安全的写时复制中的关键问题是进行更新
use count atomic,如果字符串将其实现公开给
从外部修改(和std::basic_string
一样),确保这一点
决定隔离实施(确保使用次数为1,
总是这样,所以来自外部的修改不会影响其他
实例)是原子的,标记字符串是孤立的。
(最后一点是g ++实现失败的地方:如果你试图
将字符串复制到一个线程中,然后通过operator[]
访问它
另一个,初始使用次数是1,你可能最终得到两个
共享实现副本的实例,标记为
隔离 - C ++源代码中的注释调用此方法
&安培; ldqou;孤立”)
无论如何,给定您显示的代码:使用写时复制
cow_string
,s
,s1
和s2
的实施会共享一个
实现,使用次数为3.表达式s1 + "haha"
并且s2 + "hahaha"
将分别创建一个新的临时字符串(带有
最初使用计数为1)。但我不确定你的问题是什么:你的代码永远不会修改任何字符串,所以唯一的问题是确保使用计数的更新是原子的。