2011-12-08 14 views
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"); 

其中cow_string就像萨特一个写入时复制字符串容器上GotW描述这里:http://www.gotw.ca/gotw/045.htm

  1. 如果S1的评价+ =“哈哈”和s2 + = “hahaha”被交错为非常精细的粒度并不意味着这是在cow_strings内部引用计数(取决于编译器)上创建竞争条件?

  2. 如果我尝试用互斥锁来防止竞争条件,甚至不会导致单线程程序中的自锁(这会让我的头部受伤)。例如S1制作一个内部副本并获取互斥量以减少参考计数上下文开关 S2也会生成一个内部副本并运行到互斥锁和bam自锁。

  3. (只有当第一个为真)如果我的团队的其他成员不是大师或不知道它的COW,是否有安全的方法让对象成为COW?

编辑:

为了清楚我不是很交错的表达的照片是由这个她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

重新编辑新操作符 - 是的,Sutter是对的。我们有另一种类似于我在答案中写的ABCD模式。可以按任意顺序调用T1或T2的新函数,并且可以按任何顺序调用该空间上的构造函数,但只能在调用该实体的空间后调用。所以C必须遵循A,D必须遵循B,否则编译器可以随心所欲。 – Mordachai

+0

并澄清:新的内存不分配给它分配的内存。新的完成时处于未知状态(垃圾)。只有在调用构造函数之后,该空间才会“意味”任何事物,并保持任何有意义的东西。这里的问题是资源泄漏的一个典型问题,也是使用智能指针的主要动机。 Sutter或Meyers或两者都有一个经验法则,永远不会有一个表达式,其中有多个新的表达式。 – Mordachai

回答

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的cow_string :: operator + =或s2的cow_string :: operator + =将会执行完成,然后才会开始另一个,并且只有在完成之后才会被调用。

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

香草萨特正试图解决这个问题,即子表达式不需要按照从左到右的顺序或深度第一顺序发生。相反,它们可以在函数调用自己的规则框架内以任何顺序(包括交错)发生!

最后一块很关键。它不能违反基本的调用机制,也不能违反完整的调用传递期的评估顺序。

所以,如果我们决定上述表达式具有4迷你操作:

A)“哈哈”被转换成将被交给cow_string ::运算+ =
B)同样的事情临时cow_string为“哈哈哈”
C)从A临时将移交到S1 :: + =
d)从乙临时将移交到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 *)之类的函数调用是不可交织的。运营商+ =也不是。这些是函数调用。他们的论点在被调用之前必须进行充分的评估。在外部环境的任何进一步评估可能恢复之前,呼叫必须完全完成。

这就是事情其实都是模棱两可的例子:

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

编译器可以以什么顺序来评估的参数(和参数的子表达式),在这么为所欲为任意顺序为foo选择。所以当foo()收到它时,会有什么结果是任何人的猜测(并且因编译器而异)。


至于你编辑两个新的函数作为参数的例子。

foo(new T1,new T2);

由于任何一个或两个消息都可以在构造函数之前调用,并且因为它们可以抛出,所以可能会发生内存泄漏。

如果编译器生成:

新T1
新T2
T1()
T2()

如果新T2抛出,则T1存储器丢失。没有T1的空间的所有者将释放它。

即使编译器确实调用了新的T1,T1(),新的T2 [throw's],你也可能在这里产生内存泄漏,因为没有人拥有T1占用的空间 - 并且由于T1的构造函数运行,但现在被放弃了。所以它产生的任何副作用将不会被撤消/管理/清理/等。

继续阅读香草萨特。他特殊的C++和更多特殊的C++都非常出色,深入讨论这些问题!

+0

in Sutters book更多Exceptional C++ Item 20他似乎暗示更精细的粒度。这里是作为电子书的章节http://flylib.com/books/en/3.259.1.55/1/ – odinthenerd

+0

我的想法是,如果他的例子中的两个构造函数可以拆分为分配和构造并交错订单那么可以走多远? – odinthenerd

+0

在我的答案中看到更多(不适合在这里) – Mordachai

1

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

至于线程,线程问题是对写 复制的原因之一已经失去了青睐:它仍然使用G ++,但是有一个 错误的线程处理(只能在一些非常 被触发特殊情况下)。一般来说,在写入时不难制作 线程安全的副本,并且编写高效的副本 并不难,但将两者结合几乎是不可能的。 (至少对于 的界面为std::basic_string<>。有了更合理的界面, 就不那么难了。)

中的关键问题线程安全的写入时复制正在使用计数原子的 的更新,如果字符串从外部公开其实施 修改(如不std::basic_string),确保 决定隔离实施(确保始终使用计数为1, ,以便来自外部的修改不会影响其他实例)是原子的,标记为该字符串是孤立的。 (最后一点是g ++执行失败的地方:如果您尝试 将字符串复制到一个线程中,并通过另一个中的operator[]访问它,并且初始使用计数为1,则最终可能会有两个实例共享 实例给你展示的代码被标记为C++源代码调用这个 &ldqou;孤儿” 隔离—的意见实施情况的复印件)

无论如何,:使用写入时复制 实施cow_strings,s1s2将分享一个 的实现,使用计数为3.表达式s1 + "haha"s2 + "hahaha"将分别创建一个新的临时字符串(最初的 使用次数为1)。但是我不确定你的问题是什么:你的代码永远不会修改任何字符串,所以唯一的问题是确保使用计数的更新是原子的。

+0

为了避免这种误解,我将s1 +“哈哈”改为s1 + =“哈哈”。抱歉不清楚。 – odinthenerd

+0

虽然我的原创想法还没有工作,因为S1 +“哈哈”会首先导致一个临时的内部指针,内容指向s中创建的内容,然后当添加“哈哈”时,它会做一个真正的副本,并减少裁判计数? – odinthenerd

+0

目前还不清楚你担心什么样的问题。使用计数的管理位于字符串类中,并为每个基本运算符调用函数。调用和返回都是顺序点,编译器不允许插入函数调用;如果你不关心性能,只需在输入每个函数时获得一个互斥锁,并在离开时释放它,并且你应该没有问题。 (除了性能。) –