Path: utzoo!utgpu!utstat!jarvis.csri.toronto.edu!mailrus!ncar!boulder!sunybcs!oswego!rocky.oswego.edu!dl From: dl@rocky.oswego.edu (Doug Lea) Newsgroups: comp.lang.c++ Subject: C++ constructor and function semantics Message-ID: <1105@oswego.Oswego.EDU> Date: 13 Mar 89 12:41:09 GMT Sender: news@oswego.Oswego.EDU Reply-To: dl@rocky.oswego.edu (Doug Lea) Organization: SUNY, Collego at Oswego Lines: 573 [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 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 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