[comp.lang.c++] C++ constructor and function semantics

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