I would like to fully understand what is exactly specified about how function call parameters are interleaved. It seems to me to have many implications. Take the following example:
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");
where cow_string is a Copy-On-Write string container like Sutter describes on GotW here: http://www.gotw.ca/gotw/045.htm
-
If the evaluation of
s1+="haha"ands2+="hahaha"are interleaved to a very fine granularity wouldn’t that mean that this is creating a race condition on cow_strings internal ref count (depending on the compiler)? -
If I try to protect against the race condition with a mutex couldn’t that even cause a self lock in a single threaded program (which makes my head hurt). E.g. S1 makes an internal copy and acquires the mutex to decrease the ref count context switches S2 also makes an internal copy and runs to the mutex and bam self lock.
-
(only if the first are true) Is there safe way to make an object a COW if the rest of my team are not gurus or don’t know its a COW?
Edit:
For clarity my picture of the expressions not being very interleaved was shaken by Herb Sutters example of this:
// In some header file:
void f( T1*, T2* );
// In some implementation file:
f( new T1, new T2 );
doing this:
allocate memory for the T1
construct the T1
allocate memory for the T2
construct the T2
call f()
or this:
allocate memory for the T1
allocate memory for the T2
construct the T1
construct the T2
call f()
read about it here: http://flylib.com/books/en/3.259.1.55/1/
Second Edit:
I guess I was assuming an reference counter changing function in cow_string to be inlined, which is a stupid assumption. Without that stupid assumption my question doesn’t really make much sense. Thanks for the answers though!
If your question changed to:
You’ve got a question that maybe makes a little more sense. Here the interactions between
s1 +=ands2 +=could potentially interfere if the compiler somehow interleaved their execution (presumably by throwing in extra threads).However, no, it can’t. C++ compilers don’t throw in extra threads, and they don’t 1/2 execute a method and switch to executing another.
s1‘scow_string::operator+=ors2‘scow_string::operator+=will execute to completion, and only then will the other begin, and only after both complete willmadbe called.The order of execution of the subexpressions in the call to
madare left to the compiler implementation – but they can’t interleave somehow in a single thread and standard compilers can’t throw in extra threads.Herb Sutter is trying to get across the point that the subexpressions don’t need to happen in left-to-right order, or in depth first order. Rather, that they can happen in any order (including interleaved) within the rules-framework of function calls themselves!
That last piece is critical. It cannot violate basic call mechanics, or order of evaluation of a complete argument-passing period.
So, if we decide the above expression has 4 mini-operations:
A)
"haha"is converted to a temporarycow_stringthat will be handed tocow_string::operator+=B) The same thing for
"hahaha"C) the temp from A will be handed to
S1::+=D) the temp from B will be handed to
S2::+=There are not infinite ways that this can go down, rather:
A, B, C, D
A, B, D, C
A, C, B, D
B, D, A, C
B, A, C, D
B, A, D, C
That’s it. A function call such as
cow_string(const char*)is not interleavable. Nor is operator+=. Those are function calls. Their arguments must be fully evaluated before they can be called. The call must complete fully before any further evaluation in the outer context may resume.Here’s an example where things are in fact ambiguous:
The compiler can choose in what order to evaluate the arguments (and the subexpressions in the arguments) to foo in any order it so pleases. So what a ends up with when
foo()receives it is anyone’s guess (and will vary from compiler to compiler).As to your edit example of two calls to new as arguments to a function.
because either or both
news can be called before either constructor, and because they can throw, you have the potential for a memory leak.If the compiler generates:
And if
new T2throws, then the memory forT1is lost. There is no owner of the space ofT1that will deallocate it.Even if the compiler does call
new T1,T1(),new T2throw’s, you can have a memory leak here because nobody owns the space thatT1occupies – and you can have additional problems becauseT1‘s constructor ran, but is now abandoned. So any side-effects it generated will not be undone / managed / cleaned up / etc.Keep reading Herb Sutter. His Exceptional C++ and More Exceptional C++ are excellent, and go into these issues in great depth!