Topic: Subtle bug in CD2 [class.copy]?


Author: Christopher Eltschka <celtschk@physik.tu-muenchen.de>
Date: 1997/06/09
Raw View
James Kanze wrote:

[...]

> IMHO, this does not prevent the NRVO.  In this special case, the
> compiler can prove very simply that the original object is no longer
> used: it is immediately destructed after the new object is constructed.
> Even if the objects are registering themselves, as above, no function,
> other than those called by the elided copy constructor, could possibly
> access the object in the intervening time.  (Strictly speaking, this is
> only true if the object being copied is the last object of class type
> in scope.  Otherwise, it could be potentially accessed by the
> destructors of the other objects.  I don't personally find this a large
> problem; most of the time when the NRVO is important, the object being
> copied is the only object of class type in scope.)

Well, given this, the paragraph would probably be easy to fix:

It could state that the optimization may be performed if the call
of the copy constructor is immediatly followed by the destructor of
the copied object (where "immediatly followed" may be evaluated
after an instruction reordering using the as-if rule).

Thus the RVO would still be able to performed, but the example could
not be broken, as the as-if rule would not allow the destructor call
to be moved to directly after the copy constructor: the observable
behaviour would be changed (release of memory p points to!).

This formulation would OTOH allow RVO in situations like:

class X { ... };
class Y { ... };

X f()
{
  X x;
  Y y;
  // compiler assumed to be able to prove that reordering
  // destructor calls of x and y doesn't change visual behaviour
  return x; // here RVO possible, because x.~X() may be moved to
            // ater copy by the as-if rule (because of assumption above)
}

Of course already adding cout debug statements (or assertions in debug
mode) to the destructors would disallow reordering and therefore RVO
in this case (y.~Y() would have to be called first).

Maybe a more sensible formulation can be found (s.th. like a possible
reordering under a "relaxed as-if rule", which might not actually
be allowed to be performed - because as-if rule might be broken - but
used for considering if optimization might occur)
---
[ comp.std.c++ is moderated.  To submit articles: try just posting with      ]
[ your news-reader.  If that fails, use mailto:std-c++@ncar.ucar.edu         ]
[ FAQ:      http://reality.sgi.com/employees/austern_mti/std-c++/faq.html    ]
[ Policy:   http://reality.sgi.com/employees/austern_mti/std-c++/policy.html ]
[ Comments? mailto:std-c++-request@ncar.ucar.edu                             ]





Author: fjh@mundook.cs.mu.OZ.AU (Fergus Henderson)
Date: 1997/05/23
Raw View
bparker@gil.com.au (Brian Parker) writes:

 >// example of faulty paragraph 15
 >#include <vector>
 >#include <algorithm>
 >#include <iostream>
 >#include <functional>
 >using namespace std;
 >
 >vector<int> reverse_vec(vector<int> v)
 >{
 > // v passed by-value, so we can safely modify it in-place
 > reverse(v.begin(), v.end());
 > return v;
 >}
 >
 >int main()
 >{
 > vector<int> v(4);
 > v[0] = 1; v[1] = 2; v[2] = 3; v[3] = 4;
 >
 > vector<int>::iterator it1 = v.begin();
 > vector<int>::iterator it2 = v.end();
 > // vector v is never (directly) used again, and it1 and it2 point to
 > // internal
 > // dynamically-allocated memory i.e. not to any part of the object v,
 > // so by 12.8 [class.copy] paragraph 15 the call to reverse_vec can be
 > // silently changed by the compiler to pass by-reference
 >
 > vector<int> v2 = reverse_vec(v);
 >
 > transform(it1, it2, v2.begin(), ostream_iterator<int>(cout, ","),
 >  plus<int>());
 >
 > // would expect output to be "5,5,5,5,"
 > // but [class.copy] paragraph 15 allows "8,6,4,2," to be output!

Note that the same is true even if you omit the local variables and write
this as

 int main()
 {
  vector<int> v(4);
  v[0] = 1; v[1] = 2; v[2] = 3; v[3] = 4;
  transform(v.begin(), v.end(), reverse_vec(v).begin(),
   ostream_iterator<int>(cout, ","), plus<int>());
 }

I wasn't entirely convinced until I saw this example, but this example
is quite convincing, IMHO.

If a bug like this occurs, who is at fault: the user, who wrote main(),
or the third-party library vendor who provided reverse_vec()?
How could library vendors document such usage restrictions?
With the current wording, you can't blame the compiler vendor.
I think the wording needs to change.

--
Fergus Henderson <fjh@cs.mu.oz.au>   |  "I have always known that the pursuit
WWW: <http://www.cs.mu.oz.au/~fjh>   |  of excellence is a lethal habit"
PGP: finger fjh@128.250.37.3         |     -- the last words of T. S. Garp.
---
[ comp.std.c++ is moderated.  To submit articles: try just posting with      ]
[ your news-reader.  If that fails, use mailto:std-c++@ncar.ucar.edu         ]
[ FAQ:      http://reality.sgi.com/employees/austern_mti/std-c++/faq.html    ]
[ Policy:   http://reality.sgi.com/employees/austern_mti/std-c++/policy.html ]
[ Comments? mailto:std-c++-request@ncar.ucar.edu                             ]





Author: James Kanze <james-albert.kanze@vx.cit.alcatel.fr>
Date: 1997/05/21
Raw View
Brian Parker <bparker@gil.com.au> writes:

    [...  I've cut most of the original post.  I came into this thread
 in mid-stream, and was not fully familiar with the original example.
 I've since looked them up on Deja News, and think that Brian Parker
 has a valid point.]

|>  One can, in fact, come up with counterexamples where the NRVO would fail if
|>  it was blindly applied by a compiler; the examples are completely contrived
|>  and of no practical use,  but are counterexamples nevertheless that would,
|>  IMO, require a draft conforming compiler to treat the NRVO as a non-standard
|>  optimisation. In particular, where you say that " In this special case, the
|>  compiler can prove very simply that the original object is no longer used:
|>  it is immediately destructed after the new object is constructed."; this may
|>  not be true in a garbage-collected implementation where pointers to some
|>  internal dynamically-allocated memory of the object are taken- a shared part
|>  of the object remains even after it is destructed.
|>
|>  I'll repeat here such an example I posted in the thread "NRVO-a good thing?"
|>  (this was considered to be of bad design there, but whether it is a "good
|>  design" is not an issue here- I argue simply that it is correct).
|>
|>  In this example, assume that Array is a standard concrete array class
|>  with correct copy (value) semantics. Assume that SubArray is a view on
|>  an Array with reference semantics. Further assume that Array and
|>  SubArray are reference counted so that any shared memory is
|>  automatically released when the last view on it is gone.

Regardless of whether the particular example is good programming
practice or not, I too think that it should work, if the language is to
be reasonable.  I think that a summary of the problem would be that,
although there is no problem with the merger of the declared objects
(since one will never be accessed again), this merger results in (or may
result in) a merger of the owned objects, even if both of the owned
objects *are* furthur used.  From this summary, I don't think it too
hard to conceive of reasonable programming practices where the rule
would (or might) cause problems.  (I don't think that your actual
example was that unreasonable.)

I think (although someone might come up with a counter example here)
that the problems only exist because of the fact that there are
simultaneously two different objects (without the rule); in the pure
case of NRVO, one of the objects is normally destructed immediately
after the construction of the the other, so the problem should not
occur, and the only real problem is in the wording of the paragraph,
which in fact permits more than was intended.

Or is there still something I've missed?

--
James Kanze      home:     kanze@gabi-soft.fr        +33 (0)1 39 55 85 62
                 office:   kanze@vx.cit.alcatel.fr   +33 (0)1 69 63 14 54
GABI Software, Sarl., 22 rue Jacques-Lemercier, F-78000 Versailles France
     -- Conseils en informatique industrielle --
---
[ comp.std.c++ is moderated.  To submit articles: try just posting with      ]
[ your news-reader.  If that fails, use mailto:std-c++@ncar.ucar.edu         ]
[ FAQ:      http://reality.sgi.com/employees/austern_mti/std-c++/faq.html    ]
[ Policy:   http://reality.sgi.com/employees/austern_mti/std-c++/policy.html ]
[ Comments? mailto:std-c++-request@ncar.ucar.edu                             ]





Author: bparker@gil.com.au (Brian Parker)
Date: 1997/05/22
Raw View
James Kanze <james-albert.kanze@vx.cit.alcatel.fr> wrote:

>Brian Parker <bparker@gil.com.au> writes:

>    [...  I've cut most of the original post.  I came into this thread
> in mid-stream, and was not fully familiar with the original example.
> I've since looked them up on Deja News, and think that Brian Parker
> has a valid point.]

>|>  One can, in fact, come up with counterexamples where the NRVO would fail if
>|>  it was blindly applied by a compiler; the examples are completely contrived
>|>  and of no practical use,  but are counterexamples nevertheless that would,
>|>  IMO, require a draft conforming compiler to treat the NRVO as a non-standard
>|>  optimisation. In particular, where you say that " In this special case, the
>|>  compiler can prove very simply that the original object is no longer used:
>|>  it is immediately destructed after the new object is constructed."; this may
>|>  not be true in a garbage-collected implementation where pointers to some
>|>  internal dynamically-allocated memory of the object are taken- a shared part
>|>  of the object remains even after it is destructed.
>|>
>|>  I'll repeat here such an example I posted in the thread "NRVO-a good thing?"
>|>  (this was considered to be of bad design there, but whether it is a "good
>|>  design" is not an issue here- I argue simply that it is correct).
>|>
>|>  In this example, assume that Array is a standard concrete array class
>|>  with correct copy (value) semantics. Assume that SubArray is a view on
>|>  an Array with reference semantics. Further assume that Array and
>|>  SubArray are reference counted so that any shared memory is
>|>  automatically released when the last view on it is gone.

>Regardless of whether the particular example is good programming
>practice or not, I too think that it should work, if the language is to
>be reasonable.  I think that a summary of the problem would be that,
>although there is no problem with the merger of the declared objects
>(since one will never be accessed again), this merger results in (or may
>result in) a merger of the owned objects, even if both of the owned
>objects *are* furthur used.  From this summary, I don't think it too
>hard to conceive of reasonable programming practices where the rule
>would (or might) cause problems.  (I don't think that your actual
>example was that unreasonable.)

Yes, I think that my counterexample for the NRVO is not too
far-fetched, particularly when automatic garbage collection becomes
more widely used in C++ (I have seen similar examples in Java code for
example); although garbage collection is strictly outside of the draft
standard, I thought that the intent of the draft was to allow optional
garbage collection, and certainly not to be garbage-collection
hostile.

As for the (flawed) use of paragraph 15 to elide copies on parameter
passing, I have included a shorter, more damning example at the end of
this post.

>I think (although someone might come up with a counter example here)
>that the problems only exist because of the fact that there are
>simultaneously two different objects (without the rule); in the pure
>case of NRVO, one of the objects is normally destructed immediately
>after the construction of the the other, so the problem should not
>occur, and the only real problem is in the wording of the paragraph,
>which in fact permits more than was intended.
...
>James Kanze      home:     kanze@gabi-soft.fr        +33 (0)1 39 55 85 62

Yes, the main problem is that the wording is faulty and so allows
copies to be elided when they shouldn't be.
Paragraph 15 could be fixed by including a note something like-
"In proving that an object 'never again be used', usage not only of
the object's direct memory, but also any other shared resources such
as dynamically allocated memory must be taken into account by the
compiler".

The problem with this fix is two fold-
(1) The wording I gave is not very precise, and a precise wording is
probably not possible as the concept of two objects' sharing of
resources depends entirely upon the class designer and so a general
definition can't be given.

(2) Even if a precise wording could be found, the corrected paragraph
15 could almost never be safely applied by a compiler (except in truly
trivial cases). The reason is that it is not enough for the compiler
to check for pointers to dynamically allocated memory as a class may
not store a direct pointer to the shared memory, but may instead
store, say, an integer index into a pointer lookup table. This kind of
indirection can be carried out to the nth degree, such that examples
could always be constructed that would confuse a compiler. Also, other
shared resources including file handles, graphics resources etc, also
need to be considered by the compiler. Note that this would also
prevent the automatic application of the NRVO due to the
counterexample I gave (that would probably not be detected by a
compiler).

In summary, fixing the wording of paragraph 15 would be better than
nothing as it corrects the dangerous bugs that it would otherwise
introduce, but in practical terms it would then be a rather useless
paragraph. As I've stated before, I would prefer to see paragraph 15
removed from the standard, and hence require compiler vendors to only
activate these optimisations via an explicit (non-standard) switch.

Brian Parker (bparker@gil.com.au)

--8<------------------------------------------------------------------------------------
// example of faulty paragraph 15
#include <vector>
#include <algorithm>
#include <iostream>
#include <functional>
using namespace std;

vector<int> reverse_vec(vector<int> v)
{
 // v passed by-value, so we can safely modify it in-place
 reverse(v.begin(), v.end());
 return v;
}

int main()
{
 vector<int> v(4);
 v[0] = 1; v[1] = 2; v[2] = 3; v[3] = 4;

 vector<int>::iterator it1 = v.begin();
 vector<int>::iterator it2 = v.end();
 // vector v is never (directly) used again, and it1 and it2 point to
internal
 // dynamically-allocated memory i.e. not to any part of the object v,
 // so by 12.8 [class.copy] paragraph 15 the call to reverse_vec can be
silently
 // changed by the compiler to pass by-reference

 vector<int> v2 = reverse_vec(v);

 transform(it1, it2, v2.begin(), ostream_iterator<int>(cout, ","),
plus<int>());

 // would expect output to be "5,5,5,5,"
 // but [class.copy] paragraph 15 allows "8,6,4,2," to be output!

 return 0;
}

-- end example--------------------------------------------------
---
[ comp.std.c++ is moderated.  To submit articles: try just posting with      ]
[ your news-reader.  If that fails, use mailto:std-c++@ncar.ucar.edu         ]
[ FAQ:      http://reality.sgi.com/employees/austern_mti/std-c++/faq.html    ]
[ Policy:   http://reality.sgi.com/employees/austern_mti/std-c++/policy.html ]
[ Comments? mailto:std-c++-request@ncar.ucar.edu                             ]





Author: bparker@gil.com.au (Brian Parker)
Date: 1997/05/14
Raw View
fjh@mundook.cs.mu.OZ.AU (Fergus Henderson) wrote:

>So what definition of object would you like to use instead?
>The only one I can think of that would justify the above interpretation
>is that an object is whatever the programmer wants it to be.
>But if that is the case, the compiler can't ever perform the optimization
>in question, since it can read the programmer's mind.
>So that is clearly not a reasonable interpretation.

That has actually been my point since I started this thread.
The only reasonable definition of aliasing between objects is any
shared usage of resources between them, but, as this can only be
determined by the actual object implementations themselves, a compiler
could almost never actually apply this optimisation except in the most
trivial cases.
Note that in principle this would also prevent the use of the named
return value optimisation as one can come up with contrived examples
where this issue arises (but the general consensus in the thread
"Named return value optimisation- a good thing?" is that my
counter-examples were of bad design and of no practical consequence.)

>>However, since a narrower interpretation of object, such as the
>>one you are applying, leads to the absurdity that you point out, that is
>>not a reasonable interpretation.

>I don't think what I pointed out is an absurdity, just an unfortunate
>consequence.  Broken code is unfortunate, but hardly absurd.
>The narrower interpretation of object is the one used everywhere else
>in the WP, I don't see why we should suddenly use a different
>interpretation here.

But if the draft is willing to allow optimisations to break code and
generate incorrect results, then I can think of  some much better
optimisations than paragraph 15, (in fact they would allow any
programme to run in constant time :-) ).
But seriously, I don't think that the gravity of the error in this
paragraph can be underestimated, because if one cannot rely upon
pass-by-value's not changing its argument then one just about can't
prove anything in the language, and in particular it practically
precludes the use of classes with reference semantics (see my other
postings for more plausible examples of where this causes errors).

>>The intention of this rule here is clear,
>>and it would be truly perverse for a compiler implementor to assert that
>>optimizing out the copy constructor in this example is consistent with the
>>intent of the WP.

>I think it would be quite reasonable to assert that it is consistent
>with the text of the WP.

>I don't think the people who wrote that part of the WP had this example
>in mind when they wrote it, so I don't think you can infer anything either
>way with regard to the intent of the WP with regard to examples like this.

Yes, I think that in generalising the named return value optimisation,
a serious bug was introduced into the draft, and in fact the examples
given in the draft indicate to me that this issue was overlooked.

>>Of course, it's quite possible that there are vendors
>>out there who, when faced with user complaints about overly aggressive
>>optimizations, would fall back on an interpretation like this and claim
>>that the WP gives them permission to break code.

>If that does happen, the fault will lie with the C++ committee, not
>just with the compiler vendor, because the only reasonable (IMHO)
>interpretation of the current WP is that it _does_ give them that permission.
>--
>Fergus Henderson <fjh@cs.mu.oz.au>   |  "I have always known that the pursuit

Agreed, so let's get it fixed before the draft is cast in stone.

It seems to me that there are three fixes-

(1) Just delete paragraph 15 and let the named return value
optimisation be strictly outside of the standard. Of course vendors
will still provide it as a non-standard extension of which (as I have
pointed out elsewhere) there is a precedent in such optimisations as
"assume no aliasing".

(2) Correct paragraph 15 by adding a note that says something like "To
prove that an object is "never used again", all forms of indirect
aliasing of resources between the object and other objects must be
considered. As a compiler will not be able to prove this in many
cases, this optimisation will only be applicable in very limited
circumstances".
Of couse that is not very precise- any other suggestions?

(3) This paragraph was added to allow the named return value
optimisation, so instead of trying to generalise that optimisation to
also cover the case of eliding copies of function parameters, limit it
to just the NRVO case itself.

My original preference was 1;  the committee may as well just remove
the paragraph as in principle option 2 doesn't add anything because
the optimsation effectively can't be used.
But I now think (following discussions in the "NRVO a good thing?"
thread) that there may be some benefit in option 2, because it gives a
clear warning that even explicitly written copy constructors may be
elided and so user codes must not rely upon side-effects in their
constructors. This would lead to the NRVO being applicable to all but
the most contrived examples and so would allow it to be enabled as a
(non-standard) compiler switch in almost all cases, and as a practical
matter some compiler vendors may want to have it enabled by default.

I can understand the committee's original motivation in generalising
the optimisation to the current paragraph 15 as option 3 would be a
special case wart on the language, but even this option would be
better than leaving the current faulty paragraph 15.

,Brian Parker (bparker@gil.com.au)

---
[ comp.std.c++ is moderated.  To submit articles: try just posting with      ]
[ your news-reader.  If that fails, use mailto:std-c++@ncar.ucar.edu         ]
[ FAQ:      http://reality.sgi.com/employees/austern_mti/std-c++/faq.html    ]
[ Policy:   http://reality.sgi.com/employees/austern_mti/std-c++/policy.html ]
[ Comments? mailto:std-c++-request@ncar.ucar.edu                             ]





Author: James Kanze <james-albert.kanze@vx.cit.alcatel.fr>
Date: 1997/05/17
Raw View
bparker@gil.com.au (Brian Parker) writes:

|>  But if the draft is willing to allow optimisations to break code and
|>  generate incorrect results, then I can think of  some much better
|>  optimisations than paragraph 15, (in fact they would allow any
|>  programme to run in constant time :-) ).
|>  But seriously, I don't think that the gravity of the error in this
|>  paragraph can be underestimated, because if one cannot rely upon
|>  pass-by-value's not changing its argument then one just about can't
|>  prove anything in the language, and in particular it practically
|>  precludes the use of classes with reference semantics (see my other
|>  postings for more plausible examples of where this causes errors).

I've just looked into this thread, but my impression is that paragraph
15 is that it doesn't really allow much optimization anyway.  The key
statement in question is: "if the implementation can prove that either
the original object or the copy will never again be used..."  In
practice, the implementation will never be able to prove this unless the
constructor (and all other member functions which have been called) are
visible.  It is, in fact, a rather common technique (in my code, at
least) for the constructor to "register" the object somewhere, i.e.: to
write the this pointer somewhere where other functions can get at it.
(Most such objects are not copiable anyway, but that's a question of
programming style, and not standards compliance.)

I think that the example given is wrong, since in fact, the "t.fun()"
called in "g" could very well access the "t1" in main, and the compiler
cannot (at least with only the code given) prove the contrary.

If this is not the case (and I have once again misunderstood something),
then I must agree with Brian Parker's reasoning completely.
Constructors do enrol objects in global registries.  While I cannot
think of a case off-hand where such an object would be copiable, the
fact that enrolling an object is a very reasonable thing to do, and that
copying an object is also reasonable, makes me very sceptical of
something that would make an object that does both unspecified.

|>  >>The intention of this rule here is clear,
|>  >>and it would be truly perverse for a compiler implementor to assert that
|>  >>optimizing out the copy constructor in this example is consistent with the
|>  >>intent of the WP.
|>
|>  >I think it would be quite reasonable to assert that it is consistent
|>  >with the text of the WP.
|>
|>  >I don't think the people who wrote that part of the WP had this example
|>  >in mind when they wrote it, so I don't think you can infer anything either
|>  >way with regard to the intent of the WP with regard to examples like this.
|>
|>  Yes, I think that in generalising the named return value optimisation,
|>  a serious bug was introduced into the draft, and in fact the examples
|>  given in the draft indicate to me that this issue was overlooked.

I don't think so.  I think that the only "bug" is in the example, which
is incorrect.  If Fergus Henderson is right, and the people who wrote
that part of the draft didn't have this example in mind, then the
obvious correction is to delete the example.  (A footnote mentionning
the possibility of a constructor storing the this pointer to a globally
accessible area would be nice, if only to put a break on overly
agressive implementers, but isn't really essential.)

|>  >>Of course, it's quite possible that there are vendors
|>  >>out there who, when faced with user complaints about overly aggressive
|>  >>optimizations, would fall back on an interpretation like this and claim
|>  >>that the WP gives them permission to break code.
|>
|>  >If that does happen, the fault will lie with the C++ committee, not
|>  >just with the compiler vendor, because the only reasonable (IMHO)
|>  >interpretation of the current WP is that it _does_ give them that permission.
|>  >--
|>  >Fergus Henderson <fjh@cs.mu.oz.au>   |  "I have always known that the pursuit
|>
|>  Agreed, so let's get it fixed before the draft is cast in stone.
|>
|>  It seems to me that there are three fixes-
|>
|>  (1) Just delete paragraph 15 and let the named return value
|>  optimisation be strictly outside of the standard. Of course vendors
|>  will still provide it as a non-standard extension of which (as I have
|>  pointed out elsewhere) there is a precedent in such optimisations as
|>  "assume no aliasing".
|>
|>  (2) Correct paragraph 15 by adding a note that says something like "To
|>  prove that an object is "never used again", all forms of indirect
|>  aliasing of resources between the object and other objects must be
|>  considered. As a compiler will not be able to prove this in many
|>  cases, this optimisation will only be applicable in very limited
|>  circumstances".
|>  Of couse that is not very precise- any other suggestions?
|>
|>  (3) This paragraph was added to allow the named return value
|>  optimisation, so instead of trying to generalise that optimisation to
|>  also cover the case of eliding copies of function parameters, limit it
|>  to just the NRVO case itself.
|>
|>  My original preference was 1;  the committee may as well just remove
|>  the paragraph as in principle option 2 doesn't add anything because
|>  the optimsation effectively can't be used.
|>  But I now think (following discussions in the "NRVO a good thing?"
|>  thread) that there may be some benefit in option 2, because it gives a
|>  clear warning that even explicitly written copy constructors may be
|>  elided and so user codes must not rely upon side-effects in their
|>  constructors. This would lead to the NRVO being applicable to all but
|>  the most contrived examples and so would allow it to be enabled as a
|>  (non-standard) compiler switch in almost all cases, and as a practical
|>  matter some compiler vendors may want to have it enabled by default.

My interpretation is that the current wording of paragraph 15 is very
close to option 2 as is.  The paragraph doesn't say "if the original
object is no longer refered to by its name...", it says simply
"used...".  And "used" means "used", by any means.

IMHO, this does not prevent the NRVO.  In this special case, the
compiler can prove very simply that the original object is no longer
used: it is immediately destructed after the new object is constructed.
Even if the objects are registering themselves, as above, no function,
other than those called by the elided copy constructor, could possibly
access the object in the intervening time.  (Strictly speaking, this is
only true if the object being copied is the last object of class type
in scope.  Otherwise, it could be potentially accessed by the
destructors of the other objects.  I don't personally find this a large
problem; most of the time when the NRVO is important, the object being
copied is the only object of class type in scope.)

--
James Kanze      home:     kanze@gabi-soft.fr        +33 (0)1 39 55 85 62
                 office:   kanze@vx.cit.alcatel.fr   +33 (0)1 69 63 14 54
GABI Software, Sarl., 22 rue Jacques-Lemercier, F-78000 Versailles France
     -- Conseils en informatique industrielle --
---
[ comp.std.c++ is moderated.  To submit articles: try just posting with      ]
[ your news-reader.  If that fails, use mailto:std-c++@ncar.ucar.edu         ]
[ FAQ:      http://reality.sgi.com/employees/austern_mti/std-c++/faq.html    ]
[ Policy:   http://reality.sgi.com/employees/austern_mti/std-c++/policy.html ]
[ Comments? mailto:std-c++-request@ncar.ucar.edu                             ]





Author: Brian Parker <bparker@gil.com.au>
Date: 1997/05/18
Raw View
James Kanze  wrote-
>>..
(B.P. wrote).
>|>  My original preference was 1;  the committee may as well just remove
>|>  the paragraph as in principle option 2 doesn't add anything because
>|>  the optimsation effectively can't be used.
>|>  But I now think (following discussions in the "NRVO a good thing?"
>|>  thread) that there may be some benefit in option 2, because it gives a
>|>  clear warning that even explicitly written copy constructors may be
>|>  elided and so user codes must not rely upon side-effects in their
>|>  constructors. This would lead to the NRVO being applicable to all but
>|>  the most contrived examples and so would allow it to be enabled as a
>|>  (non-standard) compiler switch in almost all cases, and as a practical
>|>  matter some compiler vendors may want to have it enabled by default.
>
>My interpretation is that the current wording of paragraph 15 is very
>close to option 2 as is.  The paragraph doesn't say "if the original
>object is no longer refered to by its name...", it says simply
>"used...".  And "used" means "used", by any means.

If that really were the meaning then I'd have no problems with paragraph 15
(except, as you rightly point out, that without seeing the constructors one
can't be sure that the draft's examples are correct even in this case). My
concern, however, is that a strict reading of the draft (and maybe even the
original interpretation) is that the paragraph does in fact mean that an
object is used only if it is used by name (or by pointer or reference to the
actual object), but it is *not* used if, say, some internal dynamically
allocated memory is accessed by another object via a pointer to that memory.
This, of course, would break a large amount of code, but it is what a strict
reading of the draft would imply- an object's memory is only the memory that
is created when it is defined- not any later dynamically allocated memory.

>IMHO, this does not prevent the NRVO.  In this special case, the
>compiler can prove very simply that the original object is no longer
>used: it is immediately destructed after the new object is constructed.
>Even if the objects are registering themselves, as above, no function,
>other than those called by the elided copy constructor, could possibly
>access the object in the intervening time.  (Strictly speaking, this is
>only true if the object being copied is the last object of class type
>in scope.  Otherwise, it could be potentially accessed by the
>destructors of the other objects.  I don't personally find this a large
>problem; most of the time when the NRVO is important, the object being
>copied is the only object of class type in scope.)
>--
>James Kanze      home:     kanze@gabi-soft.fr        +33 (0)1 39 55 85 62

One can, in fact, come up with counterexamples where the NRVO would fail if
it was blindly applied by a compiler; the examples are completely contrived
and of no practical use,  but are counterexamples nevertheless that would,
IMO, require a draft conforming compiler to treat the NRVO as a non-standard
optimisation. In particular, where you say that " In this special case, the
compiler can prove very simply that the original object is no longer used:
it is immediately destructed after the new object is constructed."; this may
not be true in a garbage-collected implementation where pointers to some
internal dynamically-allocated memory of the object are taken- a shared part
of the object remains even after it is destructed.

I'll repeat here such an example I posted in the thread "NRVO-a good thing?"
(this was considered to be of bad design there, but whether it is a "good
design" is not an issue here- I argue simply that it is correct).

In this example, assume that Array is a standard concrete array class
with correct copy (value) semantics. Assume that SubArray is a view on
an Array with reference semantics. Further assume that Array and
SubArray are reference counted so that any shared memory is
automatically released when the last view on it is gone.

Now we need a function that will return two overlapped SubArrays which
are views onto the same Array. We are not interested in the actual
shared Array, just the subarrays- but we do want to return a *copy* of
the original array for other purposes.

Array NotAContrivedFunction(SubArray& sub1, SubArray& sub2)
{
 Array temp(100,100); // we are not interested in the shared array
    // -just the SubArrays so use a local Array
    // (initialised to 0)

 sub1 = temp.GetSharedView(0,0,50,50); // sub1 now views from
      //  (0,0) to (50,50)

 sub2 = temp.GetSharedView(30,30,100,100); // sub2 now views from
      //  (30,30) to (100,100)

 return temp; // now return a *copy* of temp
}

int main( )
{
 SubArray s1, s2;
 Array acopy = NotAContrivedFunction(s1, s2);

 // modify subarray views
 s1(0,0) = 10;

 cout << acopy(0,0) << endl;
 // should output 0 but with NRVO acopy shares storage with the
 // SubArrays and hence is modified and 10 is output
}

,Brian Parker (bparker@gil.com.au)
---
[ comp.std.c++ is moderated.  To submit articles: try just posting with      ]
[ your news-reader.  If that fails, use mailto:std-c++@ncar.ucar.edu         ]
[ FAQ:      http://reality.sgi.com/employees/austern_mti/std-c++/faq.html    ]
[ Policy:   http://reality.sgi.com/employees/austern_mti/std-c++/policy.html ]
[ Comments? mailto:std-c++-request@ncar.ucar.edu                             ]





Author: petebecker@aol.com (PeteBecker)
Date: 1997/05/10
Raw View
In article <5knh99$9gv@mulga.cs.mu.OZ.AU>, fjh@mundook.cs.mu.OZ.AU (Fergus
Henderson) writes:

 >"Paul D. DeRocco" <pderocco@ix.netcom.com> writes:
 >
 > >Brian Parker wrote:
 > >>
 > >> I believe that 12.8 [class.copy], paragraph 15 was added to the draft
 > >> to allow the named return value optimisation to be performed.
However,
 > >> the way it is written goes way beyond that optimisation and, by my
 > >> reading, is just plain broken.
 > >>
 > >> To give an example of the problem, in the following a SubArray has
 > >> reference sematics to a portion of an Array-
 > >>
 > >> void g(Array t)
 > >> {
 > >>         t.SetToOne();   // set whole array to 1.0
 > >>         ...
 > >> }
 > >>
 > >> int main()
 > >> {
 > >>         Array  t2;
 > >>
 > >>         SubArray s(t2); // s is a reference to a part of t2's storage
 > >>
 > >>         g(t2);
 > >>         // t2 is not used again, so 12.8 paragraph 15 allows t2 to be
 >passed
 > >>              //  by-reference in the call to g( ) instead of by-value
 > >>
 > >>         double d = s(0,0);      // s has incorrectly been modified by
the
 >call
 > >> to
 > >>                                                      // g( )!
 > >> }
 > >>
 > >> Of course, this depends on the definition of the phrase, "the
original
 > >> object ... will never be used again". If that is interpreted to mean-
 > >> "the original object, or any portion of any memory used by the
object,
 > >> or any other shared resource (including file handles, graphics
 > >> resources etc) ... will never be used again (including simply taking
 > >> the address of a portion of memory etc.)" then it is probably OK, but
 > >> I would argue that the obvious interpretation is that in the example
 > >> above, t2 is *not* used after g( ), but s is.
 >
 >I agree with this interpretation.

Why? "t2" is not an object, it is the name of an object. Clearly the
object that "t2" refers to, namely, the area of storage created by the
declaration 'Array t2;' is used after the call to g().
    -- Pete
---
[ comp.std.c++ is moderated.  To submit articles: try just posting with      ]
[ your news-reader.  If that fails, use mailto:std-c++@ncar.ucar.edu         ]
[ FAQ:      http://reality.sgi.com/employees/austern_mti/std-c++/faq.html    ]
[ Policy:   http://reality.sgi.com/employees/austern_mti/std-c++/policy.html ]
[ Comments? mailto:std-c++-request@ncar.ucar.edu                             ]





Author: bparker@gil.com.au (Brian Parker)
Date: 1997/05/11
Raw View
petebecker@aol.com (PeteBecker) wrote:

 >In article <5knh99$9gv@mulga.cs.mu.OZ.AU>, fjh@mundook.cs.mu.OZ.AU (Fergus
 >Henderson) writes:

 > >"Paul D. DeRocco" <pderocco@ix.netcom.com> writes:
 > >
 > > >Brian Parker wrote:
 > > >>
 > > >> I believe that 12.8 [class.copy], paragraph 15 was added to the draft
 > > >> to allow the named return value optimisation to be performed.
 >However,
 > > >> the way it is written goes way beyond that optimisation and, by my
 > > >> reading, is just plain broken.
 > > >>
 > > >> To give an example of the problem, in the following a SubArray has
 > > >> reference sematics to a portion of an Array-
 > > >>
 > > >> void g(Array t)
 > > >> {
 > > >>         t.SetToOne();   // set whole array to 1.0
 > > >>         ...
 > > >> }
 > > >>
 > > >> int main()
 > > >> {
 > > >>         Array  t2;
 > > >>
 > > >>         SubArray s(t2); // s is a reference to a part of t2's storage
 > > >>
 > > >>         g(t2);
 > > >>         // t2 is not used again, so 12.8 paragraph 15 allows t2 to be
 > >passed
 > > >>              //  by-reference in the call to g( ) instead of by-value
 > > >>
 > > >>         double d = s(0,0);      // s has incorrectly been modified by
 >the
 > >call
 > > >> to
 > > >>                                                      // g( )!
 > > >> }
 > > >>
 > > >> Of course, this depends on the definition of the phrase, "the
 >original
 > > >> object ... will never be used again". If that is interpreted to mean-
 > > >> "the original object, or any portion of any memory used by the
 >object,
 > > >> or any other shared resource (including file handles, graphics
 > > >> resources etc) ... will never be used again (including simply taking
 > > >> the address of a portion of memory etc.)" then it is probably OK, but
 > > >> I would argue that the obvious interpretation is that in the example
 > > >> above, t2 is *not* used after g( ), but s is.
 > >
 > >I agree with this interpretation.

 >Why? "t2" is not an object, it is the name of an object. Clearly the
 >object that "t2" refers to, namely, the area of storage created by the
 >declaration 'Array t2;' is used after the call to g().
 >    -- Pete

I didn't make it clear in that example that I was intending that Array
used dynamically-allocated memory in its implementation and SubArray
is a reference to that internal dynamically-allocated storage *not* to
the area of storage that the definition "Array t2" directly creates.
In this case, the concept of "the object will never be used again" is
less well-defined (and is not checkable by a compiler, in general).

(see a later post by me in this thread for a clearer example)

This issue also arises for any shared resource, not just memory.

,Brian Parker (bparker@gil.com.au)
---
[ comp.std.c++ is moderated.  To submit articles: try just posting with      ]
[ your news-reader.  If that fails, use mailto:std-c++@ncar.ucar.edu         ]
[ FAQ:      http://reality.sgi.com/employees/austern_mti/std-c++/faq.html    ]
[ Policy:   http://reality.sgi.com/employees/austern_mti/std-c++/policy.html ]
[ Comments? mailto:std-c++-request@ncar.ucar.edu                             ]





Author: fjh@mundook.cs.mu.OZ.AU (Fergus Henderson)
Date: 1997/05/11
Raw View
petebecker@aol.com (PeteBecker) writes:

>fjh@mundook.cs.mu.OZ.AU (Fergus Henderson) writes:
>
> > >Brian Parker wrote:
> > >>
> > >>         Array  t2;
> > >>
> > >>         SubArray s(t2); // s is a reference to a part of t2's storage
> > >>
> > >>         g(t2);
> > >>         // t2 is not used again, so 12.8 paragraph 15 allows t2 to be
> > >>         // passed by-reference in the call to g( ) instead of by-value
> > >>
> > >>         double d = s(0,0);      // s has incorrectly been modified by
> > >>                  // the call to g()!
> > >>
> > >> I would argue that the obvious interpretation is that in the example
> > >> above, t2 is *not* used after g( ), but s is.
> >
> >I agree with this interpretation.
>
>Why? "t2" is not an object, it is the name of an object. Clearly the
>object that "t2" refers to, namely, the area of storage created by the
>declaration 'Array t2;' is used after the call to g().

I presume that in this example `Array' is a class that holds a pointer
(let's assume it is called `buf') to storage that is dynamically allocated
with `new'.  The constructor for `s' will make a copy of the pointer
`t2.buf', and the expression `s(0,0)' will access the memory pointed
to by this pointer, but although the storage at `*(t2.buf)' is used again,
the region of memory occupied by the object `t2' is not used again.
Thus although the memory that `t2' points to is used again,
the object `t2' itself is not used again.

--
Fergus Henderson <fjh@cs.mu.oz.au>   |  "I have always known that the pursuit
WWW: <http://www.cs.mu.oz.au/~fjh>   |  of excellence is a lethal habit"
PGP: finger fjh@128.250.37.3         |     -- the last words of T. S. Garp.
---
[ comp.std.c++ is moderated.  To submit articles: try just posting with      ]
[ your news-reader.  If that fails, use mailto:std-c++@ncar.ucar.edu         ]
[ FAQ:      http://reality.sgi.com/employees/austern_mti/std-c++/faq.html    ]
[ Policy:   http://reality.sgi.com/employees/austern_mti/std-c++/policy.html ]
[ Comments? mailto:std-c++-request@ncar.ucar.edu                             ]





Author: fjh@mundook.cs.mu.OZ.AU (Fergus Henderson)
Date: 1997/05/11
Raw View
Brian Parker <bparker@gil.com.au> writes:

>Fergus Henderson wrote:
>FH>Yes, but that is a bad thing.  As Brian Parker argues, the
>FH>compiler really should not be allowed to perform the optimization
>FH>in examples like this, because it leads to behaviour which the
>FH>programmer is unlikely to expect and may hence cause some very subtle
>FH>bugs.
>FH>The fact that most compilers won't perform the optimization in such
>FH>cases just means that the subtle bugs will be found when porting to a
>FH>new compiler, long after the original programmer has left.
>
>Thanks for appreciating my argument; it is more than just a "bad thing"
>though- paragraph 15 is simply incorrect.

I don't agree with that.  You could accuse it of sloppy terminology,
but it is not incorrect.

>The fundamental problem is that the "optimisation" in [class.copy] paragraph
>15 is simply incorrect

Nope, if the standard says that it is allowed, then it is allowed.
You can argue about the appropriateness of the term "optimization",
but the intent is clear: implementations are allowed to perform this
transformation even if it changes the observable behaviour of the program.

BTW, although as I said above I think it is unfortunate that the
semantics can be changed in potentially unexpected ways by this
transformation, the examples given in this thread aren't particularly
convincing, and so the performance improvement may justify the
very rare subtle bugs.

--
Fergus Henderson <fjh@cs.mu.oz.au>   |  "I have always known that the pursuit
WWW: <http://www.cs.mu.oz.au/~fjh>   |  of excellence is a lethal habit"
PGP: finger fjh@128.250.37.3         |     -- the last words of T. S. Garp.
---
[ comp.std.c++ is moderated.  To submit articles: Try just posting with your
                newsreader.  If that fails, use mailto:std-c++@ncar.ucar.edu
  comp.std.c++ FAQ: http://reality.sgi.com/austern/std-c++/faq.html
  Moderation policy: http://reality.sgi.com/austern/std-c++/policy.html
  Comments? mailto:std-c++-request@ncar.ucar.edu
]





Author: petebecker@aol.com (PeteBecker)
Date: 1997/05/12
Raw View
In article <5l4dhk$n7m@mulga.cs.mu.OZ.AU>, fjh@mundook.cs.mu.OZ.AU (Fergus
Henderson) writes:

 >petebecker@aol.com (PeteBecker) writes:
 >
 >>fjh@mundook.cs.mu.OZ.AU (Fergus Henderson) writes:
 >>
 >> > >Brian Parker wrote:
 >> > >>
 >> > >>         Array  t2;
 >> > >>
 >> > >>         SubArray s(t2); // s is a reference to a part of t2's storage
 >> > >>
 >> > >>         g(t2);
 >> > >>         // t2 is not used again, so 12.8 paragraph 15 allows t2 to be
 >> > >>         // passed by-reference in the call to g( ) instead of by-value
 >> > >>
 >> > >>         double d = s(0,0);      // s has incorrectly been modified by
 >> > >>                  // the call to g()!
 >> > >>
 >> > >> I would argue that the obvious interpretation is that in the example
 >> > >> above, t2 is *not* used after g( ), but s is.
 >> >
 >> >I agree with this interpretation.
 >>
 >>Why? "t2" is not an object, it is the name of an object. Clearly the
 >>object that "t2" refers to, namely, the area of storage created by the
 >>declaration 'Array t2;' is used after the call to g().
 >
 >I presume that in this example `Array' is a class that holds a pointer
 >(let's assume it is called `buf') to storage that is dynamically allocated
 >with `new'.  The constructor for `s' will make a copy of the pointer
 >`t2.buf', and the expression `s(0,0)' will access the memory pointed
 >to by this pointer, but although the storage at `*(t2.buf)' is used again,
 >the region of memory occupied by the object `t2' is not used again.
 >Thus although the memory that `t2' points to is used again,
 >the object `t2' itself is not used again.


    The object t2 consists of two blocks of memory: the memory occupied by
the pointer and the memory that the pointer points to. When you copy the
pointer you are creating an alias to part of the object. When you use that
alias you are using the object t2.
    Unfortunately, that doesn't fall out directly from the WP's definition
of object. However, since a narrower interpretation of object, such as the
one you are applying, leads to the absurdity that you point out, that is
not a reasonable interpretation. The intention of this rule here is clear,
and it would be truly perverse for a compiler implementor to assert that
optimizing out the copy constructor in this example is consistent with the
intent of the WP. Of course, it's quite possible that there are vendors
out there who, when faced with user complaints about overly aggressive
optimizations, would fall back on an interpretation like this and claim
that the WP gives them permission to break code.
    -- Pete
---
[ comp.std.c++ is moderated.  To submit articles: try just posting with      ]
[ your news-reader.  If that fails, use mailto:std-c++@ncar.ucar.edu         ]
[ FAQ:      http://reality.sgi.com/employees/austern_mti/std-c++/faq.html    ]
[ Policy:   http://reality.sgi.com/employees/austern_mti/std-c++/policy.html ]
[ Comments? mailto:std-c++-request@ncar.ucar.edu                             ]





Author: bparker@gil.com.au (Brian Parker)
Date: 1997/05/12
Raw View
fjh@mundook.cs.mu.OZ.AU (Fergus Henderson) wrote:

>>Thanks for appreciating my argument; it is more than just a "bad thing"
>>though- paragraph 15 is simply incorrect.
>
>I don't agree with that.  You could accuse it of sloppy terminology,
>but it is not incorrect.
>
>>The fundamental problem is that the "optimisation" in [class.copy] paragraph
>>15 is simply incorrect
>
>Nope, if the standard says that it is allowed, then it is allowed.
>You can argue about the appropriateness of the term "optimization",
>but the intent is clear: implementations are allowed to perform this
>transformation even if it changes the observable behaviour of the program.
>

That's the tragedy of it- if it is not fixed before the final standard
is released then it will be decreed to be correct even though it
isn't.

>BTW, although as I said above I think it is unfortunate that the
>semantics can be changed in potentially unexpected ways by this
>transformation, the examples given in this thread aren't particularly
>convincing, and so the performance improvement may justify the
>very rare subtle bugs.

I'll concede that in the case of the named return value optimisation
(which I believe was the original justification for paragraph 15)
(that is, eliding a copy on function return) that the issue arises
only in contrived examples and it would be uncommon in practice (in
fact would almost never occur). I cannot agree, however, that the bugs
introduced by allowing copies to be elided on function calls (i.e.
changing the sematics from pass by value to pass by reference) would
be either uncommon or subtle, in fact I would suspect that any
sizeable code that used subarrays or other types with reference
semantics would be stung by it, and in many cases it would be
difficult to work around.

e.g.
Given an Array type with value semantics and a SubArray type with
reference semantics (i.e. it acts like a built-in reference), in the
following Transpose functions, which store the transpose of "a" into
"s", "s" might be aliased with "a" so we need to make a temporary copy
of "a" first.

(Note: Array uses dynamically allocated storage and SubArray only
stores a pointer to this shared memory *not* a reference to the
original object)

inline
void Transpose1(Array a, SubArray& s)
{
        // pass "a" by value as it might be aliased with s
        // (the following code would otherwise fail in this case)
        for (int r=0; r < 100; r++) { // (assume 100x100 matrices)
                for (int c=0; c < 100; c++) {
                        s(c,r) = a(r,c);
                }
        }
}

// Transpose1 is incorrect by CD2 rules, let's try introducing an
explicit temporary
inline
void Transpose2(const Array& a, SubArray& s)
{
        Array temp(a);
        // create temp as "a" might be aliased with "s"
        // oops, still won't work by paragraph 15

        for (int r=0; r < 100; r++) { // (assume 100x100 matrices)
                for (int c=0; c < 100; c++) {
                        s(c,r) = temp(r,c);
                }
        }
}

// CD2, however, does consider aliasing due to built-in references to
be significant
inline
void Transpose3(Array a, Array& s)
{
        for (int r=0; r < 100; r++) { // (assume 100x100 matrices)
                for (int c=0; c < 100; c++) {
                        s(c,r) = a(r,c);
                }
        }
}

int main()
{
        Array a(100,100);
        SubArray s1 = a(Index(0,99), Index(0,99));
       // set s1 as an aliased view on "a"


        Transpose1(a, s1);        // will fail (intermittently) under
CD2 C++
        // (CD2 unfortunately feels free to silently change
Transpose1( ) to pass by reference)
        // (by paragraph 15, even in Transpose2( ) the compiler is
free to elide temp
        // as "a" is not used again!)

        print(s1);                // a not used again, but s1 is (cf.
print(s2) )

// uncomment this line to fix the above code
//        a(0,0);   // not a particularly obvious fix for a bug
introduced by the compiler many lines earlier!

//  However, if these lines were uncommented they would be OK-
//  as the CD2 treats built-in references with more respect.
//        Array& s2 = a;
//        Transpose3(a, s2);      // OK
//        print(s2);              //  CD2 would consider "a" to
   // be used here

}

Note that although Transpose( ) could be rewritten not to require a
temporary Array, many (most?) other functions that are sensitive to
aliasing cannot be easily rewritten without a temporary e.g. matrix
multiplication; but anyway, the point is I shouldn't have to know
about such arcane aspects of the draft as paragraph 15 to be able to
write correct code- I have passed "a" by-value in Transpose1( ) and I
would expect that to be honoured.

(also note that I used inline functions above as, in general, a
compiler will not know how a separately compiled function will be
used).

The Transpose1( ) function above is an entirely plausible function, in
fact similar to how many functions would be written to use SubArrays,
and needing to avoid such code is, IMO, indefensible.

The fundamental problem is that [class.copy] paragraph 15 assumes that
aliasing between objects does not depend on the design of the objects,
and hence can be defined by the standard for all objects (i.e. as
meaning direct aliasing between the base storage of the object). This
is patently wrong. I faced the same issue in designing my template
composite operators library (http://www.gil.com.au/~bparker/); I need
to check for aliasing between objects to know whether I can eliminate
temporaries. I have an IsAliased( ) function that can be overloaded by
types with reference semantics to do the appropriate checks for that
type and I have a default implementation that just uses address
comparison as an aliasing check. Importantly, the library checks for
aliasing even in the case where two objects are of different types,
because, as in the Array/SubArray case, they may still have aliasing.
[class.copy] paragraph 15 effectively says that in the case of the
compiler, it can always assume that objects of two different types
(with no inheritance relationship) are not aliased! If I had made that
mistake in my library I would consider it a bug, but paragraph 15
decrees that it's not so in the case of the compiler! Worse, there is
not much point in my library carefully checking for this case when the
compiler may elide the temporaries I create anyway.

I don't see how types with reference semantics can be supported by the
current CD2 draft if such a fundamental concept as aliasing between
objects is ignored by a compiler.

This seems to me to be a grave error in the draft, but as no one else
seems bothered by it, I suspect that I may have missed something and
would be more than happy to have it pointed out to me.

>... the performance improvement may justify the
>very rare subtle bugs.

I would rather have the standard be correct and then if particular
implementations want to go ahead and blindly apply these optimisations
even where they are incorrect then so be it. In particular, I would
consider this to be a reasonable approach in the case of the named
return value optimisation where this issue is almost certain not to
arise, but I suspect that a compiler that also did this "optimisation"
more generally would be rightly denounced as buggy.

,Brian Parker (bparker@gil.com.au)
---
[ comp.std.c++ is moderated.  To submit articles: Try just posting with your
                newsreader.  If that fails, use mailto:std-c++@ncar.ucar.edu
  comp.std.c++ FAQ: http://reality.sgi.com/austern/std-c++/faq.html
  Moderation policy: http://reality.sgi.com/austern/std-c++/policy.html
  Comments? mailto:std-c++-request@ncar.ucar.edu
]





Author: fjh@mundook.cs.mu.OZ.AU (Fergus Henderson)
Date: 1997/05/14
Raw View
petebecker@aol.com (PeteBecker) writes:

>    The object t2 consists of two blocks of memory: the memory occupied by
>the pointer and the memory that the pointer points to. When you copy the
>pointer you are creating an alias to part of the object. When you use that
>alias you are using the object t2.
>    Unfortunately, that doesn't fall out directly from the WP's definition
>of object.

So what definition of object would you like to use instead?
The only one I can think of that would justify the above interpretation
is that an object is whatever the programmer wants it to be.
But if that is the case, the compiler can't ever perform the optimization
in question, since it can read the programmer's mind.
So that is clearly not a reasonable interpretation.

>However, since a narrower interpretation of object, such as the
>one you are applying, leads to the absurdity that you point out, that is
>not a reasonable interpretation.

I don't think what I pointed out is an absurdity, just an unfortunate
consequence.  Broken code is unfortunate, but hardly absurd.
The narrower interpretation of object is the one used everywhere else
in the WP, I don't see why we should suddenly use a different
interpretation here.

>The intention of this rule here is clear,
>and it would be truly perverse for a compiler implementor to assert that
>optimizing out the copy constructor in this example is consistent with the
>intent of the WP.

I think it would be quite reasonable to assert that it is consistent
with the text of the WP.

I don't think the people who wrote that part of the WP had this example
in mind when they wrote it, so I don't think you can infer anything either
way with regard to the intent of the WP with regard to examples like this.

>Of course, it's quite possible that there are vendors
>out there who, when faced with user complaints about overly aggressive
>optimizations, would fall back on an interpretation like this and claim
>that the WP gives them permission to break code.

If that does happen, the fault will lie with the C++ committee, not
just with the compiler vendor, because the only reasonable (IMHO)
interpretation of the current WP is that it _does_ give them that permission.

--
Fergus Henderson <fjh@cs.mu.oz.au>   |  "I have always known that the pursuit
WWW: <http://www.cs.mu.oz.au/~fjh>   |  of excellence is a lethal habit"
PGP: finger fjh@128.250.37.3         |     -- the last words of T. S. Garp.
---
[ comp.std.c++ is moderated.  To submit articles: try just posting with      ]
[ your news-reader.  If that fails, use mailto:std-c++@ncar.ucar.edu         ]
[ FAQ:      http://reality.sgi.com/employees/austern_mti/std-c++/faq.html    ]
[ Policy:   http://reality.sgi.com/employees/austern_mti/std-c++/policy.html ]
[ Comments? mailto:std-c++-request@ncar.ucar.edu                             ]





Author: fjh@mundook.cs.mu.OZ.AU (Fergus Henderson)
Date: 1997/05/06
Raw View
"Paul D. DeRocco" <pderocco@ix.netcom.com> writes:

 >Brian Parker wrote:
 >>
 >> I believe that 12.8 [class.copy], paragraph 15 was added to the draft
 >> to allow the named return value optimisation to be performed. However,
 >> the way it is written goes way beyond that optimisation and, by my
 >> reading, is just plain broken.
 >>
 >> To give an example of the problem, in the following a SubArray has
 >> reference sematics to a portion of an Array-
 >>
 >> void g(Array t)
 >> {
 >>         t.SetToOne();   // set whole array to 1.0
 >>         ...
 >> }
 >>
 >> int main()
 >> {
 >>         Array  t2;
 >>
 >>         SubArray s(t2); // s is a reference to a part of t2's storage
 >>
 >>         g(t2);
 >>         // t2 is not used again, so 12.8 paragraph 15 allows t2 to be passed
 >>              //  by-reference in the call to g( ) instead of by-value
 >>
 >>         double d = s(0,0);      // s has incorrectly been modified by the call
 >> to
 >>                                                      // g( )!
 >> }
 >>
 >> Of course, this depends on the definition of the phrase, "the original
 >> object ... will never be used again". If that is interpreted to mean-
 >> "the original object, or any portion of any memory used by the object,
 >> or any other shared resource (including file handles, graphics
 >> resources etc) ... will never be used again (including simply taking
 >> the address of a portion of memory etc.)" then it is probably OK, but
 >> I would argue that the obvious interpretation is that in the example
 >> above, t2 is *not* used after g( ), but s is.

I agree with this interpretation.

 >I would expect the compiler not to do the optimization in that
 >particular case; the fact that the object was passed somewhere by
 >reference means that it can no longer prove that it isn't used.

Your expectation is wrong.  The only place where `t2' is passed by
reference in the original source code is in the constructor for `s',
and if the constructor for `s' is inline, then the compiler might well
be able to do the optimization.

 >> IMHO in a function call, pass-by-value should always mean that and not
 >> "pass-by-value most times but randomly pass-by-reference when the
 >> compiler gets the urge"; if a compiler could (somehow) determine that
 >> a pass-by-value could be changed to pass-by-reference with *no*
 >> sematic changes then it is already allowed to do so under the "as-if"
 >> rules.
 >
 >The "as if" rule isn't sufficient for describing this situation. This
 >paragraph is making it clear that it is perfectly legal to do this
 >optimization, even though it involves one less constructor/destructor
 >call.

Yes, but that is a bad thing.  As Brian Parker argues, the
compiler really should not be allowed to perform the optimization
in examples like this, because it leads to behaviour which the
programmer is unlikely to expect and may hence cause some very subtle
bugs.

The fact that most compilers won't perform the optimization in such
cases just means that the subtle bugs will be found when porting to a
new compiler, long after the original programmer has left.

--
Fergus Henderson <fjh@cs.mu.oz.au>   |  "I have always known that the pursuit
WWW: <http://www.cs.mu.oz.au/~fjh>   |  of excellence is a lethal habit"
PGP: finger fjh@128.250.37.3         |     -- the last words of T. S. Garp.
---
[ comp.std.c++ is moderated.  To submit articles: try just posting with      ]
[ your news-reader.  If that fails, use mailto:std-c++@ncar.ucar.edu         ]
[ FAQ:      http://reality.sgi.com/employees/austern_mti/std-c++/faq.html    ]
[ Policy:   http://reality.sgi.com/employees/austern_mti/std-c++/policy.html ]
[ Comments? mailto:std-c++-request@ncar.ucar.edu                             ]





Author: bparker@gil.com.au (Brian Parker)
Date: 1997/05/06
Raw View
Alexandre Oliva <oliva@dcc.unicamp.br> wrote:

>Brian Parker writes:

>> void g(Array t)
>> {
>>  t.SetToOne(); // set whole array to 1.0
>>  ...
>> }

>> int main()
>> {
>>  Array  t2;

>>  SubArray s(t2); // s is a reference to a part of t2's storage
>
>Here, if t2 is passed by reference and the compiler does not know the
>definition of SubArray's constructor, it won't be able to prove that
>t2 will never be used again, since a reference or pointer to it could
>have been saved in the constructor, so the optimization you're worried
>about would not be valid.

OK, that's not a good example; change it to-

void g(Array t)
{
  t.SetToOne(); // set whole array to 1.0
  ...
 }

int main()
{
 Array t2;

 double* p = t2.GetPointerToRawMemory( ); // store a pointer to t2's
    //internal dynamically allocated memory

  g(t2);
  // t2 is not used again, so 12.8 paragraph 15 allows t2 to be
passed
              //  by-reference in the call to g( ) instead of by-value

 SubArray s(p + 20); // initialise subarray with shared raw data
    // (assumed to be unchanged as t2 was
    // passed by value in g( ) )
  double d = s(0,0); // s has incorrectly been modified by
the call
    //  to g( )!
}

In this example, t2 is definitely not used again- no pointers or
references to it are taken, but a completely different result occurs
when the optimisation is applied. This is using the straightforward
interpretation of the draft's "the original object or the copy will
never be used again".

>Even if t2 were passed by value to the constructor, there could still
>be a reference or pointer to a shared object in the copy of t2 passed
>to SubArray's constructor, and this shared object could be referred to
>in both s and the copy to be passed to g.  In this case, it would be
>much harder to prove that any such member object would not be affected
>by the change, and compilers should not risk doing the optimization.

>IMO, there's just a small set of circumstances where this optimization
>can be done.  Anyway, these are quite common cases, and they are
>rather simple to prove correct and the optimization is worth the
>trouble.  More complicated cases, where a proof of non-use and
>non-aliasing is harder or more arguable, need not be covered by
>compilers and probably won't be by most of them.
>--
>Alexandre Oliva

Here you are interpreting the draft as meaning: "the original object
or the copy, *or any resources shared by these objects* will never be
used again" (which I agree is the only reasonable interpretation).
But in this case I don't see how the optimisation's safe use could be
proved by the compiler except in completely trivial cases. As in the
above example, calling any member function that returns a value could
lead to resource sharing (a pointer was returned in the above
example's GetPointerToRawMemory( ) but it could just have easily have
been a pointer cast to an int, or a shared file handle that is
indistinguishable from an int). Even if a member function is not
called, the constructor may have stored a pointer to its internal
memory in a global variable which could lead to sharing. Or perhaps a
custom memory manager that allows access to previously allocated
memory has replaced the default manager (a contrived example, but
legal). All of these situations can potentially lead to resource
sharing between objects in a manner undetectable by the compiler.
Basically only classes that didn't use dynamically allocated memory,
and had only trivial constructors, and that basically aren't used at
all before they were passed to a function would be suitable candidates
for this optimisation as far as I can see. Note that this also
excludes the raison d'etre for this part of the draft- the use of the
NRV optimisation for non-trivial examples.
(See my post "NRV optimisation- a good thing?" for an example of where
the  NRV optimisation fails due to unexpected aliasing of file
handles).

In summary, there are just so many subtle ways that aliasing can
manifest itself between objects in a manner undetectable by a compiler
that I don't see how this part of the standard could be safely used.
What are some examples of situations where it could be used safely and
are the speed-ups obtainable in these cases worth the change in
semantics the optimisation entails?

,Brian Parker (bparker@gil.com.au)
---
[ comp.std.c++ is moderated.  To submit articles: Try just posting with your
                newsreader.  If that fails, use mailto:std-c++@ncar.ucar.edu
  comp.std.c++ FAQ: http://reality.sgi.com/austern/std-c++/faq.html
  Moderation policy: http://reality.sgi.com/austern/std-c++/policy.html
  Comments? mailto:std-c++-request@ncar.ucar.edu
]





Author: Brian Parker <bparker@gil.com.au>
Date: 1997/05/07
Raw View
Fergus Henderson wrote:
(BP = Brian Parker, PD = Paul DeRocco, FH = Fergus Henderson)

BP>>> Of course, this depends on the definition of the phrase, "the original
BP>>> object ... will never be used again". If that is interpreted to mean-
BP>>> "the original object, or any portion of any memory used by the object,
BP>>> or any other shared resource (including file handles, graphics
BP>>> resources etc) ... will never be used again (including simply taking
BP>>> the address of a portion of memory etc.)" then it is probably OK, but
BP>>> I would argue that the obvious interpretation is that in the example
BP>>> above, t2 is *not* used after g( ), but s is.

FH>I agree with this interpretation.

PD>>I would expect the compiler not to do the optimization in that
PD>>particular case; the fact that the object was passed somewhere by
PD>>reference means that it can no longer prove that it isn't used.

FH>Your expectation is wrong.  The only place where `t2' is passed by
FH>reference in the original source code is in the constructor for `s',
FH>and if the constructor for `s' is inline, then the compiler might well
FH>be able to do the optimization.

Yes, the constructor call for s in my example is a red herring. Replace the
posted example with the following-

 void g(Array t)
{
         t.SetToOne();   // set whole array to 1.0
         ...
}

int main()
{
        // (assume that Array has a copy-constructor that does a deep copy)
         Array  t2;

        // store a pointer to the internal dynamically-allocated storage of t2
         double * p = t2.GetPointerToRawMemory();

        g(t2);
        // t2 is not used again, so 12.8 paragraph 15 allows t2 to be passed
        //  by-reference in the call to g( ) instead of by-value

        // attach a SubArray to look at a part of t2's storage
        // (we passed by value above so we would expect that t2's storage is
unchanged)
         SubArray s(p + 20);

         double d = s(0,0);      // s has incorrectly been modified by the call
                                 // g( )!
}


BP>>> IMHO in a function call, pass-by-value should always mean that and not
BP>>> "pass-by-value most times but randomly pass-by-reference when the
BP>>> compiler gets the urge"; if a compiler could (somehow) determine that
BP>>> a pass-by-value could be changed to pass-by-reference with *no*
BP>>> sematic changes then it is already allowed to do so under the "as-if"
BP>>> rules.
>>
PD>>The "as if" rule isn't sufficient for describing this situation. This
PD>>paragraph is making it clear that it is perfectly legal to do this
PD>>optimization, even though it involves one less constructor/destructor
PD>>call.

FH>Yes, but that is a bad thing.  As Brian Parker argues, the
FH>compiler really should not be allowed to perform the optimization
FH>in examples like this, because it leads to behaviour which the
FH>programmer is unlikely to expect and may hence cause some very subtle
FH>bugs.
FH>The fact that most compilers won't perform the optimization in such
FH>cases just means that the subtle bugs will be found when porting to a
FH>new compiler, long after the original programmer has left.

Thanks for appreciating my argument; it is more than just a "bad thing"
though- paragraph 15 is simply incorrect. Imagine trying to do a formal
proof on the (modified) example I have just posted. If one can't rely on the
basic axiom that pass by value won't modify its argument then all bets are off!

The fundamental problem is that the "optimisation" in [class.copy] paragraph
15 is simply incorrect unless it can be proved not only that "the original
object or the copy will never be used again" but also that no resource used
by the original object or the copy will ever be used again, where "resource"
includes memory, file handles, graphics resources etc. But worse is the fact
that changing paragraph 15 to include this proviso won't help- it is almost
impossible for a compiler to rule out aliasing or sharing of resources
between objects except in the most trivial cases. As shown in the above
example, simply returning a pointer from a member function could lead to
aliasing, but it could just as easily have returned a pointer cast to an
int. Even if no member functions are called between the object's definition
and the function call, the constructor may have squirreled away a pointer to
some internal memory in a global variable, or perhaps the default memory
allocator has been replaced by a custom version that allows one to get
access to previously allocated memory (contrived but legal). And other
resources such as file handles are completely indistinguishable from ints
(see my example posted in the thread "NRV- a good thing?" for an example
where a compiler is almost certain to miss the aliasing of file handles when
applying the NRV optimisation- leading to incorrect results).

By the time one excludes classes using dynamically allocated memory; or with
non-trivial constructors; or that have their member data accessed (and so
on..), then only the most trivial cases are left, which defeats the raison
detre of this paragraph- the named return value optimisation, as for these
trivial cases it could just as easily use a "computational constructor".

Also note that my concerns here are quite separate from the fact that this
optimisation may elide side-effects in copy-constructors. Whether it is
appropriate for an "optimisation" to change semantics even in this fashion
is debatable, but is not what I am discussing here (BTW I don't think it is
appropriate).

Personally, I don't mind a language having a few syntactic ambiguities (as
C++ has) but please don't destroy the fundamental semantic axioms we rely
upon (I'm a big fan of pass-by-value).

,Brian Parker (bparker@gil.com.au)

(BTW: I posted a response to Oleg Zablud with most of these comments, but my
news server has been down intermittently over the past few days and so I am
not sure if that response was posted. My apologies if these comments are
repeated.)
---
[ comp.std.c++ is moderated.  To submit articles: Try just posting with your
                newsreader.  If that fails, use mailto:std-c++@ncar.ucar.edu
  comp.std.c++ FAQ: http://reality.sgi.com/austern/std-c++/faq.html
  Moderation policy: http://reality.sgi.com/austern/std-c++/policy.html
  Comments? mailto:std-c++-request@ncar.ucar.edu
]





Author: bparker@gil.com.au (Brian Parker)
Date: 1997/05/04
Raw View

I believe that 12.8 [class.copy], paragraph 15 was added to the draft
to allow the named return value optimisation to be performed. However,
the way it is written goes way beyond that optimisation and, by my
reading, is just plain broken.

It states that-
"Whenever a class object is copied and the original and the copy have
the same type, if the implementation can prove that either the
original object or the copy will never be used again except as the
result of an implicit destructor call, an implementation is permitted
to treat the original and the copy as two different ways of referring
to the same object and not perform the copy at all."

To give an example of the problem, in the following a SubArray has
reference sematics to a portion of an Array-

void g(Array t)
{
 t.SetToOne(); // set whole array to 1.0
 ...
}

int main()
{
 Array  t2;

 SubArray s(t2); // s is a reference to a part of t2's storage

 g(t2);
 // t2 is not used again, so 12.8 paragraph 15 allows t2 to be passed
             //  by-reference in the call to g( ) instead of by-value

 double d = s(0,0); // s has incorrectly been modified by the call
to
                                                     // g( )!
}

Of course, this depends on the definition of the phrase, "the original
object ... will never be used again". If that is interpreted to mean-
"the original object, or any portion of any memory used by the object,
or any other shared resource (including file handles, graphics
resources etc) ... will never be used again (including simply taking
the address of a portion of memory etc.)" then it is probably OK, but
I would argue that the obvious interpretation is that in the example
above, t2 is *not* used after g( ), but s is.

Note that even if the first interpretation were used, it seems to me
that in general a compiler could not know what memory or resources an
object uses- it may initialise ints to store pointers or XOR them etc,
file handles may have multiple representations and may be
indistinguisahble from ints etc. (Note: an example of where file
handles may be important is where a function g( ) may change the
current position in the file, so a copy of an object would create its
own copy of the file handle to avoid interactions between the files).

IMHO in a function call, pass-by-value should always mean that and not
"pass-by-value most times but randomly pass-by-reference when the
compiler gets the urge"; if a compiler could (somehow) determine that
a pass-by-value could be changed to pass-by-reference with *no*
sematic changes then it is already allowed to do so under the "as-if"
rules.

It seems to me that if compilers started using the second
interpretation, this paragraph could cause more subtle bugs than the
"noalias" proposal rejected in the C standard.

Any comments?
(BTW, I hope I have just misread the draft and would be more than
happy to have my errors pointed out to me)

Thanks,
Brian Parker (bparker@gil.com.au)

---
[ comp.std.c++ is moderated.  To submit articles: try just posting with      ]
[ your news-reader.  If that fails, use mailto:std-c++@ncar.ucar.edu         ]
[ FAQ:      http://reality.sgi.com/employees/austern_mti/std-c++/faq.html    ]
[ Policy:   http://reality.sgi.com/employees/austern_mti/std-c++/policy.html ]
[ Comments? mailto:std-c++-request@ncar.ucar.edu                             ]





Author: Alexandre Oliva <oliva@dcc.unicamp.br>
Date: 1997/05/05
Raw View

Brian Parker writes:

> void g(Array t)
> {
>  t.SetToOne(); // set whole array to 1.0
>  ...
> }

> int main()
> {
>  Array  t2;

>  SubArray s(t2); // s is a reference to a part of t2's storage

Here, if t2 is passed by reference and the compiler does not know the
definition of SubArray's constructor, it won't be able to prove that
t2 will never be used again, since a reference or pointer to it could
have been saved in the constructor, so the optimization you're worried
about would not be valid.

Even if t2 were passed by value to the constructor, there could still
be a reference or pointer to a shared object in the copy of t2 passed
to SubArray's constructor, and this shared object could be referred to
in both s and the copy to be passed to g.  In this case, it would be
much harder to prove that any such member object would not be affected
by the change, and compilers should not risk doing the optimization.

IMO, there's just a small set of circumstances where this optimization
can be done.  Anyway, these are quite common cases, and they are
rather simple to prove correct and the optimization is worth the
trouble.  More complicated cases, where a proof of non-use and
non-aliasing is harder or more arguable, need not be covered by
compilers and probably won't be by most of them.

>  g(t2);
>  // t2 is not used again, so 12.8 paragraph 15 allows t2 to be passed
>              //  by-reference in the call to g( ) instead of by-value

>  double d = s(0,0); // s has incorrectly been modified by the call
> to
>                                                      // g( )!
> }

--
Alexandre Oliva
mailto:oliva@dcc.unicamp.br mailto:aoliva@acm.org
Universidade Estadual de Campinas, SP, Brasil
---
[ comp.std.c++ is moderated.  To submit articles: try just posting with      ]
[ your news-reader.  If that fails, use mailto:std-c++@ncar.ucar.edu         ]
[ FAQ:      http://reality.sgi.com/employees/austern_mti/std-c++/faq.html    ]
[ Policy:   http://reality.sgi.com/employees/austern_mti/std-c++/policy.html ]
[ Comments? mailto:std-c++-request@ncar.ucar.edu                             ]





Author: "Paul D. DeRocco" <strip_these_words_pderocco@ix.netcom.com>
Date: 1997/05/05
Raw View

Brian Parker wrote:
>
> I believe that 12.8 [class.copy], paragraph 15 was added to the draft
> to allow the named return value optimisation to be performed. However,
> the way it is written goes way beyond that optimisation and, by my
> reading, is just plain broken.
>
> To give an example of the problem, in the following a SubArray has
> reference sematics to a portion of an Array-
>
> void g(Array t)
> {
>         t.SetToOne();   // set whole array to 1.0
>         ...
> }
>
> int main()
> {
>         Array  t2;
>
>         SubArray s(t2); // s is a reference to a part of t2's storage
>
>         g(t2);
>         // t2 is not used again, so 12.8 paragraph 15 allows t2 to be passed
>              //  by-reference in the call to g( ) instead of by-value
>
>         double d = s(0,0);      // s has incorrectly been modified by the call
> to
>                                                      // g( )!
> }
>
> Of course, this depends on the definition of the phrase, "the original
> object ... will never be used again". If that is interpreted to mean-
> "the original object, or any portion of any memory used by the object,
> or any other shared resource (including file handles, graphics
> resources etc) ... will never be used again (including simply taking
> the address of a portion of memory etc.)" then it is probably OK, but
> I would argue that the obvious interpretation is that in the example
> above, t2 is *not* used after g( ), but s is.

I would expect the compiler not to do the optimization in that
particular case; the fact that the object was passed somewhere by
reference means that it can no longer prove that it isn't used.

> IMHO in a function call, pass-by-value should always mean that and not
> "pass-by-value most times but randomly pass-by-reference when the
> compiler gets the urge"; if a compiler could (somehow) determine that
> a pass-by-value could be changed to pass-by-reference with *no*
> sematic changes then it is already allowed to do so under the "as-if"
> rules.

The "as if" rule isn't sufficient for describing this situation. This
paragraph is making it clear that it is perfectly legal to do this
optimization, even though it involves one less constructor/destructor
call.

--

Ciao,
Paul

(Please send e-mail to mailto:pderocco@ix.netcom.com instead of the
return address, which has been altered to foil junk mail senders.)
---
[ comp.std.c++ is moderated.  To submit articles: Try just posting with your
                newsreader.  If that fails, use mailto:std-c++@ncar.ucar.edu
  comp.std.c++ FAQ: http://reality.sgi.com/austern/std-c++/faq.html
  Moderation policy: http://reality.sgi.com/austern/std-c++/policy.html
  Comments? mailto:std-c++-request@ncar.ucar.edu
]