dl@rocky.oswego.edu (Doug Lea) (03/13/89)
[Please accept my apologies for the length of this posting, but it could not have been much shorter than it is.] ----------- Contents Part 1. A Quiz. Part 2. The Answers. Part 3. Are the answers wrong? Part 4. Idempotent constructors. Part 5. Volatile constructors. Part 6. A Proposal. Part 7. A Related proposal. Part 8. Appendix ----------- Part 1. A Quiz Try predicting the results of the following program. The (empirical) answers are given below. #include <stream.h> class X { public: int a; X() { cout << "X() "; a = 1; } X(int b) { cout << "X(int) "; a = b; } X(X& y) { cout << "X(X&) "; a = y.a + 1; } void operator = (X& y) { cout << "X=X "; a = y.a + 100; } void operator = (int b) { cout << "X=int "; a = b + 100; } }; X f() { return 1; } // or, equivalently, `return X(1)' X g() { X a ; return a; } X h() { return g(); } X i() { X a = g(); return a; } X j(X a) { return a; } X k(X a) { return j(a); } main() { cout << "a: "; X a; cout << a.a << "\n"; cout << "\n"; cout << "vaC: "; X vaC(a); cout << vaC.a << "\n"; cout << "vaE: "; X vaE = a; cout << vaE.a << "\n"; cout << "vaEC: "; X vaEC = X(a); cout << vaEC.a << "\n"; cout << "vaDE: "; X vaDE; vaDE = a; cout << vaDE.a << "\n"; cout << "\n"; cout << "v1C: "; X v1C(1); cout << v1C.a << "\n"; cout << "v1E: "; X v1E = 1; cout << v1E.a << "\n"; cout << "v1EC: "; X v1EC = X(1); cout << v1EC.a << "\n"; cout << "v1DE: "; X v1DE; v1DE = 1; cout << v1DE.a << "\n"; cout << "\n"; cout << "vfC: "; X vfC(f()); cout << vfC.a << "\n"; cout << "vfE: "; X vfE = f(); cout << vfE.a << "\n"; cout << "vfEC: "; X vfEC = X(f()); cout << vfEC.a << "\n"; cout << "vfDE: "; X vfDE; vfDE = f(); cout << vfDE.a << "\n"; cout << "\n"; cout << "vgC: "; X vgC(g()); cout << vgC.a << "\n"; cout << "vgE: "; X vgE = g(); cout << vgE.a << "\n"; cout << "vgEC: "; X vgEC = X(g()); cout << vgEC.a << "\n"; cout << "vgDE: "; X vgDE; vgDE = g(); cout << vgDE.a << "\n"; cout << "\n"; cout << "vhC: "; X vhC(h()); cout << vhC.a << "\n"; cout << "vhE: "; X vhE = h(); cout << vhE.a << "\n"; cout << "vhEC: "; X vhEC = X(h()); cout << vhEC.a << "\n"; cout << "vhDE: "; X vhDE; vhDE = h(); cout << vhDE.a << "\n"; cout << "\n"; cout << "viC: "; X viC(i()); cout << viC.a << "\n"; cout << "viE: "; X viE = i(); cout << viE.a << "\n"; cout << "viEC: "; X viEC = X(i()); cout << viEC.a << "\n"; cout << "viDE: "; X viDE; viDE = i(); cout << viDE.a << "\n"; cout << "\n"; cout << "vjC: "; X vjC(j(a)); cout << vjC.a << "\n"; cout << "vjE: "; X vjE = j(a); cout << vjE.a << "\n"; cout << "vjEC: "; X vjEC = X(j(a)); cout << vjEC.a << "\n"; cout << "vjDE: "; X vjDE; vjDE = j(a); cout << vjDE.a << "\n"; cout << "\n"; cout << "vkC: "; X vkC(k(a)); cout << vkC.a << "\n"; cout << "vkE: "; X vkE = k(a); cout << vkE.a << "\n"; cout << "vkEC: "; X vkEC = X(k(a)); cout << vkEC.a << "\n"; cout << "vkDE: "; X vkDE; vkDE = k(a); cout << vkDE.a << "\n"; } ---------- Part 2. The Answers Here's what happens. The results are the same under both g++-1.34.0 and cfront 1.2. (I manually aligned the numbers for easier reading) a: X() 1 vaC: X(X&) 2 vaE: X(X&) 2 vaEC: X(X&) 2 vaDE: X() X=X 101 v1C: X(int) 1 v1E: X(int) 1 v1EC: X(int) 1 v1DE: X() X=int 101 vfC: X(int) X(X&) 2 vfE: X(int) 1 vfEC: X(int) X(X&) 2 vfDE: X() X(int) X=X 101 vgC: X() X(X&) X(X&) 3 vgE: X() X(X&) 2 vgEC: X() X(X&) X(X&) 3 vgDE: X() X() X(X&) X=X 102 vhC: X() X(X&) X(X&) 3 vhE: X() X(X&) 2 vhEC: X() X(X&) X(X&) 3 vhDE: X() X() X(X&) X=X 102 viC: X() X(X&) X(X&) X(X&) 4 viE: X() X(X&) X(X&) 3 viEC: X() X(X&) X(X&) X(X&) 4 viDE: X() X() X(X&) X(X&) X=X 103 vjC: X(X&) X(X&) X(X&) 4 vjE: X(X&) X(X&) 3 vjEC: X(X&) X(X&) X(X&) 4 vjDE: X() X(X&) X(X&) X=X 103 vkC: X(X&) X(X&) X(X&) X(X&) 5 vkE: X(X&) X(X&) X(X&) 4 vkEC: X(X&) X(X&) X(X&) X(X&) 5 vkDE: X() X(X&) X(X&) X(X&) X=X 104 ----------- Part 3. Are the Answers Right? These results seem indicative of some possible problems with C++ function and constructor semantics and/or current implementations of the corresponding constructs. Probably the biggest surprise to you was the fact that for functions, but not variables, X var(fun()) behaves differently than X var = fun() in that the former invokes one more X(X&) constructor than the latter. This fact is emphasized by the demonstration that X var(fun()) does behave the same as X var = X(fun()) The fourth line of each set is just provided to show that the use of `operator =' is not at all involved in this issue. (I should also note here that none of this involves any interaction with corresponding destructor sequences. In both cfront and g++, the destructor sequences operate in just the way one would expect, given the corresponding constructor sequences.) My best explanation for the difference between `X var(fun())' and `X var = fun()' is that it represents an implementation error. While I cannot find a single sentence in Stroustrup's book that unambiguously states that these two forms have equivalent effects, many examples and discussions treat the forms as equivalent. I will henceforth assume this to be the case. Of course, the interesting question is, which behavior is `correct'? That is, should `X var(fun())' behave like the current `X var = fun()' or vice versa? Other points of interest in these examples will turn out to also rest on this issue. The answer to this rests on a deeper, but simple issue. ----------- Part 4. Idempotent Constructors First, I need to sneak in a bit of notation. I'll say that `a === b' if any possible sequence of C++ computations using object `a' will display the exact same visible behavior as any sequence using `b'. That is to say, a === b, means that a and b contain precisely the same information but could differ in their machine addresses, the addresses pointed to by any of their member variables, etc. I do know that this definition is not quite careful enough, and is possibly contentious, but this is as precise as I need to be. I will call any function for which a === fun(a) `idempotent', which seems not to be an abuse of the term. Note that this notion goes beyond the idea that a function is `pure': it says that *nested* occurrences of `fun' have the same effect, i.e, that it is a `true' identity function in that a === fun(a) === fun(fun(a))... Now, the only question that needs to be resolved in order to discover how these examples `should' behave is: *** Does C++ implicitly require that the X(X&) constructor be idempotent, *** *** thus necessitating that for any X a, a === X(a) === X(X(a)) ... ? *** Note that this was *not* the case in the above example. Note also that for classes without any pointers as member variables, the C++ default compiler-generated bitwise-copy version of X(X&) *is* idempotent. If the answer is `yes', I have a few suggestions about C++ compiler implementation matters: 1) That X(const X&) be treated as a `pure function' of a const argument. No side effects to the argument, globals, or static class variables are to be allowed. Any given compiler may or may not actually be able to enforce this. However, at the least, a warning message should be given if the constructor were not declared as `X(const X&)'. In fact, pragmatically, and given the `spirit' of C++, it would probably even be desirable if the compiler did not enforce this, since programmers should probably be able to write code with side effects that contain potential, but not actual computational consequences. In this case, C++ syntax could allow programmers to specifically declare constructors as `const X(const X&)' if they would like the compiler to help verify purity. 2) That any compiler should elide nested X(X&) constructors. That is, it may treat anything of the form `X(X(expr))' as just `X(expr)'. This answers the question posed above: All three of `X var(fun())', `X var = fun()', and `X var = X(fun())' should run through the same constructor sequence as does the current `X var = fun()'. Of course, given (1), it really couldn't matter (except efficiency-wise), since all three results would be the same anyway. (And the above examples would behave unpredictably since they would be illegal to begin with.) Again, compilers may be limited in their ability to perform this. In the above examples, the same constructor sequence could occur for all of the examples using functions `g()', `h()', and `i()', but this might be extremely difficult for compilers to discover. This issue is discussed further below. Note that neither of these changes would detrimentally impact current users of current C++ compilers so long as all constructors were idempotent already. Also, as we've seen, current compilers make the effects of using non-idempotent constructors pretty senseless anyway! ----------- Part 5. Volatile Constructors However, if the answer is `no', several changes in current semantics and implementations must be introduced. I'll call `non-idempotent' constructors `volatile' (A better term might be `opaque', but `volatile' is already a C/C++ keyword and has a very similar meaning, which I exploit below.) First, a bit of motivation about why someone might desire volatile X(X&) constructors: There now exist a collection of C++ programming `tricks' devised by just about everyone who implements `value oriented' classes (to use Jerry Schwarz's apt term) in order to avoid the `useless' copies of data held by objects in various calculations that would otherwise be forced via X(X&) constructors. Such classes include things like matrices, strings, etc., that support many `constructive' operations, and thus many function calls. There are a couple of good techniques for maintaining some bookkeeping information (reference counts, `temp' counts, etc.) within objects such that the results of `temporary' calculations and the like can be reused across constructors and other function calls in a way that is transparent to the users of such classes. The validity of this kind of bookkeeping information is often very dependent on knowledge of the precise constructor sequences encountered across declarations, by-value function calls, and by-value function returns. If X(X&) constructors are required to be idempotent, then some of these techniques become invalid. This would not be the end of the world -- There are other methods available that approach the power of these techniques that could be be adopted, with the cost of some unavoidable inconveniences on the part of both implementors and users of such code. (A hint about how to do this is implicit in Part 8.) On the other hand, if X(X&) is allowed to be volatile, several changes would be required in C++ implementations. The basic idea is that any compiler would be forced to assume that the X(X&) constructor is `just another function' with thoroughly unknown properties. Thus the usual rules of evaluation would apply. Specifically, the standard `innermost-first' evaluation rule must be used, as in: 1) The declaration `X var(f())' should continue to exhibit its current behavior. That is, the sequence of events should be 1. evaluate f() into a temporary via the function-evaluation rules that require that a temporary (call it `T1') be created to hold the return value of `f()'. 2. construct `var' from `T1' via X(X&) 2) As discussed above, `X var = f()' should have the same semantics as in (1) 3) The declaration `X var = X(f())' should evaluate f() into T1, construct a `T2' from `T1', then finally, `var' from `T2'. 4) Function h X h() { return g(); } must 1. evaluate g() into a temporary `T1' 2. construct the return value of h via X(X&) on `T1' (This means that `h()' should work like the current function `i()'.) 5) function k X k(X a) { return j(a); } must 1. use X(X&) on `a' into `T1' and pass in as j()'s `a' 2. evaluate `j()' into a new `T2' 3. construct the return value of k via X(X&) on `T2'. The example in Part 8 shows how this behavior can be approximated within current implementations of C++ via a couple of tricks. Again, there is nothing very new or special about the rules for `volatile' semantics: they are the constructor versions of standard C/C++ (or just about any other Algol-based language) function semantics. ----------- Part 6. A proposal There can be no compromise: either X(X&) is idempotent or it isn't! On the other hand, C++ does not have to adopt exactly one or the other semantics, it could adopt both by allowing programmers to indicate of the nature of the X(X&) constructor. In fact, this is my proposal: that C++ semantics be those described for idempotent X(X&) constructors, *unless* an X(X&) constructor is explicitly labeled as volatile (as in `volatile X(X&)'), in which case the the corresponding `volatile' semantics should be invoked. I believe that this is the best possible solution. Why? 1) Historical precedent. Current C++ compilers behave as if they believe that X(X&) is idempotent already in some respects. Thus, they already perform (albeit sometimes strangely) actions that are not inconsistent with the idempotent case. Support for volatile constructors could be added in at some point. (Although, to me this seems a bit backwards: It seems cleaner for a compiler to initially treat constructors as volatile, and then push the internal representations through an `elision' procedure. A note for compiler construction enthusiasts: elision algorithms seem to be unexplored territory, although classic common subexpression analysis algorithms could probably be tailored for these purposes.) 2) Efficiency: If X(X&)'s *are* idempotent, compilers will be allowed to perform more optimizations via elision than they do now. On the other hand, if programmers create volatile X(X&)'s with the intent of actually improving performance by intercepting constructors, and `stealing' rather than copying data, etc., then the compiler will be able to assist in this effort in a predictable fashion. 3) Logical coherence. Among other things, these rules would make the semantics of of construction from function results in either case more clearly resemble those for construction from variables. I plead, however, that if this proposal is not adopted, then at least the idempotent version be officially sanctioned! Again this would not *require* any changes in existing compilers, but would serve to dissuade programmers from writing code that in any way involves `constructor-counting', and to make clear the kinds of optimizations compilers could legally perform. ----------- Part 7. A related proposal A secondary issue about efficiency arises out of these considerations. How can programmers assist compilers in eliminating `useless' X(X&) constructors, especially in the idempotent case? Here is a further proposal that I think goes a long way toward solving this. Consider a function `m()' X m() { X b; b.a = 23; return b; } and the declaration X v = m(); As has been implicit above, what happens here is that `m()' secretly has an argument, the address of the return value. At invocation, the address of enough space to hold `v' is sent in as the hidden argument. Then `b' is constructed and its `b' field is set to the value 23. Then an X(X&) constructor is applied to `b', with the hidden return value location as the target, so that `v' is now bound to the return value. But this is pretty wasteful. The local `b' is declared just to hold something that will be copied right out. While a compiler that combined an `elision' algorithm with interprocedural data flow analysis could conceivably eliminate all of this, it seems much more desirable to allow programmers to assist the compiler in generating efficient code by somehow manipulating the return value explicitly, thus avoiding the local variable and X(X&) constructor all together. A simple-looking and appealing solution to this is to allow programmers to name, and thus explicitly manipulate the return value. Michael Tiemann has partially implemented this idea as an `experimental extension' of the g++ compiler. (Warning to g++ users: do not use this feature in 1.34.0: it doesn't quite work yet in the way I describe!) I would like to argue for its `official' adoption. The new syntax (optionally, of course) enables a programmer to declare the return value in almost just the way you declare a local, but as part of the declaration header instead of the code part. For example, X m() return r; { r.a = 23; } says that m returns an X, that we are calling `r'. The declaration of `r' is a standard proper declaration, whose effects are executed *before* any of the { ... } part of m. Only `return' or falling-off-the-edge returns are legal ways to return out of a function declared in this fashion. Thus, things like X m() return r(23) { return; } or even X m() return r(23) { } are perfectly legal, while things like X m() return r; { X b; return b; } are senseless. For classes in which the X(X&) constructor incurs a heavy copying penalty, (and especially in the usual case where there is a quick `X()' constructor), this is a major savings: At least one X(X&) constructor is avoided. The only disadvantage of this extension is that programmers do not have any control over when the constructor for the return value is called: It must be at the beginning. However, there is a compensating benefit: Functions written in this fashion are actually safer than those written in the standard fashion, since there can never be cases where no `return x' is encountered across some path of computation, thus causing `garbage' to be returned, which can and does happen in normal functions. Again, this syntax should co-exist with, not supplant, the standard syntax. There are plenty of cases where the standard syntax would continue to be most preferable and/or efficient. A very important example of this is demonstrated with function `h()', above, X h() { return g(); } Assuming idempotency semantics, current C++ implementations already correctly elide two implicit X(X&)'s. It appears that implementing named return values within current C++ compilers is fairly straightforward. There are surely other ways to establish the syntax and behavior of this kind of construct, but none as simple and easily implementable. Thanks for reading this far! Comments would be very much appreciated. ---------- Part 8. Appendix Try this out. It simulates volatile constructors by creating a second class, `Y' which is identical to X in all but name, and coding things such that X(Y&)'s and Y(X&)'s alternate. Warning: You are due for at least one more surprise that emphasizes my claim that current C++ implementations (inconsistently) assume idempotency. (As a further experiment, see what happens if you alter the declaration `class Y' to `class Y : public X'. You might want to make additional changes from there, like removing X(Y&)...) #include <stream.h> class Y; class X { public: int a; X() { cout << "X() "; a = 1; } X(int b) { cout << "X(int) "; a = b; } X(X& y) { cout << "X(X&) "; a = y.a + 1; } X(Y& y); // below void operator = (X& y) { cout << "X=X "; a = y.a + 100; } void operator = (int b) { cout << "X=int "; a = b + 100; } }; class Y { public: int a; Y() { cout << "Y() "; a = 1; } Y(int b) { cout << "Y(int) "; a = b; } Y(Y& y) { cout << "Y(Y&) "; a = y.a + 1; } Y(X& y) { cout << "Y(X&) "; a = y.a + 1; } void operator = (Y& y) { cout << "Y=Y "; a = y.a + 100; } void operator = (int b) { cout << "Y=int "; a = b + 100; } }; X::X(Y& y) { cout << "X(Y&) "; a = y.a + 1; } Y f() { return 1; } Y g() { X a ; return a; } X g2() { return g(); } Y h() { return g2(); } Y i() { X a = g(); return a; } Y j(X a) { return a; } X j2(Y a){ return a; } Y k(Y a) { return j2(a); } main() { // precisely the same code as in Part 1 } Answers: a: X() 1 vaC: X(X&) 2 vaE: X(X&) 2 vaEC: X(X&) 2 vaDE: X() X=X 101 v1C: X(int) 1 v1E: X(int) 1 v1EC: X(int) 1 v1DE: X() X=int 101 vfC: Y(int) X(Y&) 2 vfE: Y(int) X(Y&) 2 vfEC: Y(int) X(Y&) 2 vfDE: X() Y(int) X(Y&) X=X 102 vgC: X() Y(X&) X(Y&) 3 vgE: X() Y(X&) X(Y&) 3 vgEC: X() Y(X&) X(Y&) 3 vgDE: X() X() Y(X&) X(Y&) X=X 103 vhC: X() Y(X&) X(Y&) Y(X&) X(Y&) 5 vhE: X() Y(X&) X(Y&) Y(X&) X(Y&) 5 vhEC: X() Y(X&) X(Y&) Y(X&) X(Y&) 5 vhDE: X() X() Y(X&) X(Y&) Y(X&) X(Y&) X=X 105 viC: X() Y(X&) X(Y&) Y(X&) X(Y&) 5 viE: X() Y(X&) X(Y&) Y(X&) X(Y&) 5 viEC: X() Y(X&) X(Y&) Y(X&) X(Y&) 5 viDE: X() X() Y(X&) X(Y&) Y(X&) X(Y&) X=X 105 vjC: X(X&) Y(X&) X(Y&) 4 vjE: X(X&) Y(X&) X(Y&) 4 vjEC: X(X&) Y(X&) X(Y&) 4 vjDE: X() X(X&) Y(X&) X(Y&) X=X 104 vkC: Y(X&) Y(Y&) X(Y&) Y(X&) X(Y&) 6 vkE: Y(X&) Y(Y&) X(Y&) Y(X&) X(Y&) 6 vkEC: Y(X&) Y(Y&) X(Y&) Y(X&) X(Y&) 6 vkDE: X() Y(X&) Y(Y&) X(Y&) Y(X&) X(Y&) X=X 106 Doug Lea, Computer Science Dept., SUNY Oswego, Oswego, NY, 13126 (315)341-2367 email: dl@rocky.oswego.edu or dl%rocky.oswego.edu@nisc.nyser.net UUCP :...cornell!devvax!oswego!dl or ...rutgers!sunybcs!oswego!dl
jss@hector.UUCP (Jerry Schwarz) (03/14/89)
In article <1105@oswego.Oswego.EDU> dl@rocky.oswego.edu (Doug Lea) writes: > >My best explanation for the difference between `X var(fun())' and >`X var = fun()' is that it represents an implementation >error. ... > >Of course, the interesting question is, which behavior is `correct'? >That is, should `X var(fun())' behave like the current `X var = fun()' >or vice versa? Other points of interest in these examples will turn >out to also rest on this issue. The answer here is (and should be) that either behavior is correct. What is happening is that in one case the compiler has decided to put the return value into a temporary before constructing "var" from that temporary and in the other case it has decided not to. One difference between X(X&) and ordinary functions is that C++ may call X(X&) when it constructs temporaries and that the number of temporaries (of type X) that are required to evaluate any particular statement is undefined. When writing X(X&) the programmer must be aware of the possibility that it may be used to construct temporaries and take that into account. Doug has inverted the possible "optimizations". In no case may a compiler elide an explicit call to a constructor, but it may insert calls that are not explicitly there. "Optimization" consists in inserting as few extra calls as possible. By the way, if X::~X() (the destructor) is declared it must eventually be called on any temporaries. >First, a bit of motivation about why someone might desire volatile >X(X&) constructors: There now exist a collection of C++ programming >`tricks' devised by just about everyone who implements `value >oriented' classes (to use Jerry Schwarz's apt term) in order to avoid >the `useless' copies of data held by objects in various calculations >that would otherwise be forced via X(X&) constructors. ... > >The validity of this kind of bookkeeping information is often very >dependent on knowledge of the precise constructor sequences encountered >across declarations, by-value function calls, and by-value function >returns. > The "tricks" are akin to reference counting. (There is more to it, but this is not the place to discuss details.) Because C++ guarantees that a constructor will be called for any space it uses as an X (e.g. the space used for function arguments) it is possible to write such code so that it is correct whatever sequences are actually invoked. Although the efficiency of the resulting code may be lowered if the C++ compiler chooses to create a lot of temporaries. Jerry Schwarz AT&T Bell Labs, Murray Hill
dl@rocky.oswego.edu (Doug Lea) (03/15/89)
Jerry Schwarz replies to my posting on constructor semantics: >>The answer here is (and should be) that either behavior is correct. >>What is happening is that in one case the compiler has decided to put >>the return value into a temporary before constructing "var" from that >>temporary and in the other case it has decided not to. OK, even though I strongly object to the implication that temporary creation during object construction is nondeterministic, let's take it from there to get the same conclusions: * In forms like X var(fun()) (among other constructs) compilers may optionally evaluate functions into temporaries. * Such evaluation results in the insertion of nested X(X&) constructors. * The behavior of a correct C++ program cannot depend on whether such temporaries are created or not. * Hence, X(X&) constructors must be idempotent (thus also `pure'). * Hence, (among other things) * Any implicit or explicit form of X(X(...X(a)...)) may be elided into just X(a). * Programmers ought be able to assist in this effort via named return values. * Establishing an alternative `volatile' semantics deserves serious consideration. I should elaborate on a few understressed points in my original posting: Assuming the idempotency rule, I am not at all convinced that compilers should *enforce* either purity or idempotency in X(X&) constructors. As I mentioned, it is probably desirable to allow programmers to write code containing side effects with potential, but not actual computational consequences especially with respect to correct performance across nested X(X&)'s. I would, however like to see the `const X(const X&)' syntax supported, so that smart compilers would be allowed to assist programmers in ascertaining the purity of constructors, which would be very helpful for those attempting to verify the correctness of class behavior. In any case, programmers must be made aware that C++ compilers do implicitly assume idempotency. Someone correctly pointed out that this issue is not unrelated to the fact that side effects within functions are *always* risky in C++ and most other languages. For example in `int a = func1() + func2()', since order of evaluation of operands is undefined, if `func1' contains a side effect affecting `func2', and vice versa, then the result of the expression is ill-defined. Language definitions for C, C++, etc., merely warn programmers that such constructs are to be avoided, without otherwise outlawing side effects. The two differences between this and the X(X&) issue are that (1) this is not at all the same kind of nondeterminism: X(X&) constructors are always `inserted' in *nested* fashion X(X(...X(a)...), so there is no ambiguity about evaluation order, assuming conformity to the innermost-first `applicative' function evaluation rule central to any Algol-type language, and (2) programmers *cannot* avoid nested X(X&) constructors since they are inserted and/or elided automatically by compilers. Thus, again, my main conclusions hold. I should also have mentioned that the named return value construct saves not only an X(X&), but also the corresponding ~X() destructor. Jerry continues, in discussing storage management techniques based on constructor sequences, >>that a constructor will be called for any space it uses as an X (e.g. >>the space used for function arguments) it is possible to write such >>code so that it is correct whatever sequences are actually invoked. >>Although the efficiency of the resulting code may be lowered if the >>C++ compiler chooses to create a lot of temporaries. Right. Try explaining this to applications programmers who find that that their 1000 X 1000 element matrices are being copied two or three times on their way out of functions! (or that `a = b + c + d;' for matrices requires more data copying than logically necessary, or...) Like I said, there *are* good ways to deal with such problems, but some of the best ways require a better specification of function and constructor semantics than C++ and/or its implementations currently provide. Semantic precision is not just a `nice' attribute of a language -- It enables programmers to write better code! (Along these lines, I wonder just how many existing C++ programs will `break' if/when cfront and g++ make `X var(fun())' operationally equivalent to `X var = fun()'.) Readers of this newsgroup may expect a few additional postings on some further language clarifications and the like that I think are necessary (or at least desirable) in order to better fulfill Stroustrup's (and my and probably your) hopes that C++ really can serve as a basis for creating special-purpose value-oriented classes that are as natural, efficient, and correct as possible in any language. I hope these things are accepted not as ultimatums or whinings, but as earnest attempts to provide useful and friendly feedback. Doug Lea, Computer Science Dept., SUNY Oswego, Oswego, NY, 13126 (315)341-2367 email: dl@rocky.oswego.edu or dl%rocky.oswego.edu@nisc.nyser.net UUCP :...cornell!devvax!oswego!dl or ...rutgers!sunybcs!oswego!dl
turner@sdti.SDTI.COM (Prescott K. Turner) (03/21/89)
The notion that X var(fun()); should be equivalent to X var = fun(); is a red herring in this discussion -- there are other cases where cfront makes a more blatant distinction between them (as when "fun" returns a type which can be converted to X in 2 steps but not 1). In article <1149@oswego.Oswego.EDU> dl@rocky.oswego.edu (Doug Lea) writes: > (Along these lines, I wonder just how many existing C++ programs will > `break' if/when cfront and g++ make `X var(fun())' operationally > equivalent to `X var = fun()'.) Such a change is not necessary, because the latter syntax will serve. It is not desirable, because it complicates the meaning of initializer: ( expresson-list ) which means Initialize the declared object using the constructor determined by the expression-list as a list of arguments. In most circumstances, I think the elmination of calls to X(X&) is a good thing. To the extent that cfront and g++ handle it, it appears easier to implement than other aspects of C++. As Lea points out, users could benefit a lot from standardizing on this behavior. -- Prescott K. Turner, Jr. Software Development Technologies, Inc. 375 Dutton Rd., Sudbury, MA 01776 USA (508) 443-5779 UUCP: ...{harvard,mit-eddie}!sdti!turner Internet: turner@sdti.sdti.com