Google Groups no longer supports new Usenet posts or subscriptions. Historical content remains viewable.
Dismiss

Guru of the Week #55: Solution

3 views
Skip to first unread message

Herb Sutter

unread,
May 24, 1999, 3:00:00 AM5/24/99
to
-------------------------------------------------------------------
Guru of the Week problems and solutions are posted regularly on
news:comp.lang.c++.moderated. For past problems and solutions
see the GotW archive at www.peerdirect.com. (c) 1999 H.P.Sutter
News archives may keep copies of this article.
-------------------------------------------------------------------

_______________________________________________________

GotW #55: Equivalent Code?
(originally proposed by Christoph Schlenker)

Difficulty: 5 / 10
_______________________________________________________


Congratulations to Mark Strecker and Ali Cehreli, the
Gurus of the Week!

I see that people took the JG question a lot farther
than I'd intended. Okay, I'll bite and include a
similar level of detail in the solution... but the main
point of this GotW was intended to be what I'll explain
under Question #3.


>JG Question
>-----------
>
>1. Describe what the following code does:
>
> // Example 1
> //
> f( a++ );
>
> Be as complete as you can about all possibilities.

A comprehensive list would be daunting, but here are
the main possibilities.

First, f could be any of the following:

1. A macro. In this case the statement could mean
just about anything, and "a++" could be evaluated
many times or not at all. For example:

#define f(x) x // once
#define f(x) (x,x,x,x,x,x,x,x,x) // 9 times
#define f(x) // not at all

GUIDELINE: Avoid using macros. They usually make
code more difficult to understand, and
therefore more troublesome to maintain.

2. A function. In this case, first "a++" is evaluated
and then the result is passed to the function as its
parameter. Normally, postincrement returns the
previous value of a in the form of a temporary
object, so f() could take its parameter either by
value or by reference to const, but not by reference
to non-const because a reference to non-const cannot
be bound to a temporary object.

3. An object. In this case, f() would be a functor,
that is, an object for which operator()() is
defined. Again, if postincrement returns the
previous value of a (as postincrement always should)
then f's operator()() could take its parameter
either by value or by reference to const.

4. A type name. In this case, the statement first
evaluates "a++" and uses the result of that
expression to initialize a temporary object of type
f.

Next, a could be:

1. A macro. In this case, again, a could mean just
about anything.

2. An object (possibly of a built-in type). In this
case, it must be a type for which a suitable
operator++(int) postincrement operator is defined.

Normally, postincrement should be implemented in
terms of preincrement and should return the previous
value of a:

// Canonical form of postincrement:
T T::operator++(int)
{
T old( *this ); // remember our original value
++*this; // always implement postincrement
// in terms of preincrement
return old; // return our original value
}

When you overload an operator, of course, you do
have the option of changing its normal semantics to
do "something unusual." For example, the following
is likely to break the Example 1 code for most kinds
of f, assuming a is of type A:

void A::operator++(int) // doesn't return anything

Don't do that. Instead, follow this sound advice:

GUIDELINE: Always preserve natural semantics for
overloaded operators. "Do as the ints
do," that is, follow the semantics of
the builtin types.

3. A value, such as an address. For example, a could be
the name of an array, or it could be a pointer.


>Guru Questions
>--------------

For the remaining questions, I will make the
simplifying assumptions that:

- f() is not a macro; and

- a is an object with natural postincrement semantics.


>2. What is the difference, if any, between the
> following two code fragments?
>
> // Example 2(a)
> //
> f( a++ );

This performs the steps:
1. a++: Increment a and return the old value.
2. f(): Execute f(), passing it the old value of a.

Example 2(a) ensures that the postincrement is
performed, and therefore a gets its new value, before
f() executes. As noted above, f() could still be a
function, a functor, or a type name which leads to a
constructor call.

Some coding standards state that operations like ++
should always appear on separate lines, on the grounds
that it can be dangerous to perform multiple operations
like ++ in the same statement because of sequence
points (more about this in GotW #56). Instead, such
coding standards would recommend:

> // Example 2(b)
> //
> f( a );
> a++;

This performs the steps:
1. f(): Execute f(), passing it the old value of a.
2. a++: Increment a and return the old value, which
is then ignored.

In both cases, f() gets the old value of a. "So what's
the big difference?" you may ask. Well, Example 2(b)
will not always have the same effect as that in Example
2(a), because Example 2(b) ensures that the
postincrement is performed, and therefore a gets its
new value, after f() executes. "But so what?" you may
insist still, now somewhat exasperated. "What's the
difference? What are you on about here?"

Clearly, if f() and a.operator++(int) have visible side
effects, the order in which they are executed can
matter. But, more specifically, consider what happens
if f() has a side effect that affects the state of a
itself: That's neither farfetched nor unlikely, and it
can happen even if f() doesn't and can't directly
change a, as I'll illustrate with an example:


>3. In Question #2, make the simplifying assumption that
> f() is a function that takes its argument by value.
> Now what is the difference, if any, between Example
> 2(a) and Example 2(b)?

The difference is that, for perfectly normal C++ code,
Example 2(a) can be legal when Example 2(b) is not.
Specifically, consider what happens when we replace f
with list::erase(), and a with list::iterator. Now the
first form is valid:

// Example 3(a)
//
// l is a list<int>
// i is a valid non-end iterator into l

l.erase( i++ ); // OK

But the second form is not:

// Example 3(b)
//
// l is a list<int>
// i is a valid non-end iterator into l

l.erase( i );
i++; // error, i is not a valid iterator

The reason that Example 3(b) is incorrect is that the
call to "l.erase( i )" invalidates i, and therefore
you can no longer call operator++ on i afterwards.


Scissors, Traffic, and Iterators
--------------------------------

Warning: Some programmers routinely write code like
Example 3(b), perhaps because of coding guidelines that
have a blanket policy of discouraging operations like
++ in function call statements.

If you're one of the programmers who writes code like
Example 3(b), you may even be routinely getting away
with it (and not realizing the danger) just because it
happens to work on the current version of your compiler
and library. But be warned: Code like Example 3(b) is
not portable, it is not sanctioned by the Standard, and
it's likely to turn and bite you when you port to
another compiler platform or even just upgrade the one
you're working on today. When it does bite, it will
bite hard, because "using-an-invalid-iterator" bugs can
be very difficult to find (unless you have the joy of
working with a good checked library implementation
during debugging -- but if you're in this situation you
must not be using a checked implementation or else it
would already have warned you about this!).

Some mothers (who are also software engineers) give the
following three pieces of good advice, and we should
always strive to follow them for our own good:

1. Don't run with scissors.

2. Don't play in traffic.

3. Don't use invalid iterators.

Next time: With the exception of examples like the
above, we'll see why it's still a good idea in general
to avoid writing operations like ++ in function calls.


---
Herb Sutter (mailto:hsu...@peerdirect.com)

PeerDirect Inc. 2695 North Sheridan Way, Suite 150
www.peerdirect.com Mississauga Ontario Canada L5K 2N6

[ Send an empty e-mail to c++-...@netlab.cs.rpi.edu for info ]
[ about comp.lang.c++.moderated. First time posters: do this! ]

Andrei Alexandrescu

unread,
May 24, 1999, 3:00:00 AM5/24/99
to
In article <374ba005...@nntp.netcom.ca>,

hsu...@peerdirect.com (Herb Sutter) wrote:
> new value, after f() executes. "But so what?" you may
> insist still, now somewhat exasperated. "What's the
> difference? What are you on about here?"

Strange you didn't mention the obvius difference related to exceptions.
I was sure you will say something about it.
If f() throws an exception, in the first case a still will be
incremented, and in the second case it will not. That's an important and
simple difference. Maybe I skimmed the post too fast?

Andrei


--== Sent via Deja.com http://www.deja.com/ ==--
---Share what you know. Learn what you don't.---

Mark Strecker

unread,
May 24, 1999, 3:00:00 AM5/24/99
to

Herb Sutter wrote:

> MESSAGE PORTION DELETED

> // Example 3(a)
> //
> // l is a list<int>
> // i is a valid non-end iterator into l
>
> l.erase( i++ ); // OK
>
> But the second form is not:
>
> // Example 3(b)
> //
> // l is a list<int>
> // i is a valid non-end iterator into l
>
> l.erase( i );
> i++; // error, i is not a valid iterator
>
> The reason that Example 3(b) is incorrect is that the
> call to "l.erase( i )" invalidates i, and therefore
> you can no longer call operator++ on i afterwards.
>

A way to avoid the problem of invalidating iterators is to use the
generic algorithms remove and remove_if. This way you can erase elements
and you don't need to worry about which container type you have. (The
method of erasing elements, i.e. l.erase( i++ );, will not work for all
containers, e.g. vectors and deques.)

Your remove will look like this :

list l;
...
l.erase(remove(l.begin(), l.end(), <match value>), l.end());

The call to remove will move all elements that match to the end of the
container and return an iterator to the 'new end' of the container. The
call to erase will perform the actual element deletion from the 'new
end' to the 'old end' of the container.

One caveat : I have had trouble with template instantiation on the major
unix platforms when using remove with map and mutlimap. But the
non-associative containers I tried worked. (I don't remember if I tried
sets.)

Enjoy.
Mark

Siemel Naran

unread,
May 24, 1999, 3:00:00 AM5/24/99
to
On 24 May 1999 16:22:04 -0400, Andrei Alexandrescu
> hsu...@peerdirect.com (Herb Sutter) wrote:

>> new value, after f() executes. "But so what?" you may
>> insist still, now somewhat exasperated. "What's the
>> difference? What are you on about here?"

>Strange you didn't mention the obvius difference related to exceptions.


>I was sure you will say something about it.
>If f() throws an exception, in the first case a still will be
>incremented, and in the second case it will not. That's an important and
>simple difference. Maybe I skimmed the post too fast?

Indeed, H Sutter did not mention anything of the case where 'a' is
a non-local variable. The example was
Thing a;
void f(Thing);
void function1() { f(a); ++a; }
void function2() { f(a++); }
int main(int argc, char * * argv) { /* call function1 or function2 */ }
If f throws, then the state of 'a' after function1 is different from the
state of 'a' after function2. If 'a' were a local variable, then this
difference wouldn't matter. But as 'a' is a global variable, the
difference does matter.

Also, what is operator++ or ++operator throw but f(...) does not? Then
once again, function1 and function2 are different.

To be fair, H Sutter did say that if f(...) and operator++ have side
effects, then function1 and function2 are different. In my definition,
the mere act of accessing a non-local variable is a side effect. For
example, writing stuff to the screen is a side effect -- you are
accessing global variables that represent pixels on the screen. Or
making a random number based on the time is a side effect -- you are
accessing a global variables that represents the current time.

--
----------------------------------
Siemel B. Naran (sbn...@uiuc.edu)
----------------------------------

0 new messages