函数调用参数中的表达式是什么粒度交错的?

时间:2011-12-08 14:14:35

标签: c++ specifications copy-on-write

我想完全理解有关如何交错函数调用参数的确切内容。在我看来,有很多含义。请看以下示例:

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

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

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

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

  4. 编辑:

    为了清楚起见,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中的引用计数器更改函数被内联,这是一个愚蠢的假设。没有这个愚蠢的假设我的问题并没有多大意义。谢谢你的答案!

2 个答案:

答案 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空间的所有者不会解除分配。

即使编译器确实调用T1T1new 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_stringss1s2的实施会共享一个 实现,使用次数为3.表达式s1 + "haha" 并且s2 + "hahaha"将分别创建一个新的临时字符串(带有 最初使用计数为1)。但我不确定你的问题是什么:你的代码永远不会修改任何字符串,所以唯一的问题是确保使用计数的更新是原子的。