www.digitalmars.com         C & C++   DMDScript  

digitalmars.D.learn - Scope of temporaries as function arguments

reply Nick Sabalausky <SeeWebsiteToContactMe semitwist.com> writes:
Probably a silly question, but I wanted to double-check...

If you have this:

    struct Foo {...}
    bar(Foo());

Then regardless of optimizations (aside from any optimizer bugs, of
course) the Foo temporary can't go out of scope or have its dtor called
until bar finishes executing, right?

Or I guess more accurately, is there any guarantee that the assert in
func() below should always pass?:

    class Foo {
        int i = 1;
        //...etc...
    }

    struct Bar {
        Foo foo;
        ~this() {
            foo.i = 2;
        }
        //...etc...
    }

    void func(Bar bar)
    {
        //...anything here that *doesn't* change bar.foo.i...

        assert(bar.foo.i == 1);  // Guaranteed to pass?
    }

    void main() {
        Foo f = new Foo();
        func(Bar(f));
    }
Jun 27 2013
parent reply "Maxim Fomin" <maxim maxim-fomin.ru> writes:
On Friday, 28 June 2013 at 04:54:56 UTC, Nick Sabalausky wrote:
 Probably a silly question, but I wanted to double-check...

 If you have this:

     struct Foo {...}
     bar(Foo());

 Then regardless of optimizations (aside from any optimizer 
 bugs, of
 course) the Foo temporary can't go out of scope or have its 
 dtor called
 until bar finishes executing, right?
Struct dtor is always called in the end of the caller (bar in example). This will be OK, but in general case no. Currently object is copied in caller side but destroyed in callee side, and if one of the arguments next to struct is passed by invoking function which throws, callee and respectively dtor will never be called.
 Or I guess more accurately, is there any guarantee that the 
 assert in
 func() below should always pass?:

     class Foo {
         int i = 1;
         //...etc...
     }

     struct Bar {
         Foo foo;
         ~this() {
             foo.i = 2;
         }
         //...etc...
     }

     void func(Bar bar)
     {
         //...anything here that *doesn't* change bar.foo.i...

         assert(bar.foo.i == 1);  // Guaranteed to pass?
     }

     void main() {
         Foo f = new Foo();
         func(Bar(f));
     }
Here yes, but in general case if there are other arguments, and one if them passed by lambda invocation which touches f and modifies, then no.
Jun 27 2013
next sibling parent "Maxim Fomin" <maxim maxim-fomin.ru> writes:
On Friday, 28 June 2013 at 05:11:11 UTC, Maxim Fomin wrote:
 Struct dtor is always called in the end of the caller (bar in 
 example).
Callee of course.
Jun 27 2013
prev sibling parent reply Nick Sabalausky <SeeWebsiteToContactMe semitwist.com> writes:
On Fri, 28 Jun 2013 07:11:05 +0200
"Maxim Fomin" <maxim maxim-fomin.ru> wrote:

 On Friday, 28 June 2013 at 04:54:56 UTC, Nick Sabalausky wrote:
 Probably a silly question, but I wanted to double-check...

 If you have this:

     struct Foo {...}
     bar(Foo());

 Then regardless of optimizations (aside from any optimizer 
 bugs, of
 course) the Foo temporary can't go out of scope or have its 
 dtor called
 until bar finishes executing, right?
Struct dtor is always called in the end of the caller (bar in example). This will be OK, but in general case no. Currently object is copied in caller side but destroyed in callee side, and if one of the arguments next to struct is passed by invoking function which throws, callee and respectively dtor will never be called.
Interesting. BTW, for anyone else reading, I just searched bugzilla and it looks Kinda ugly as it means refcounted RAII structs can leek under this condition: func(refCountedStruct, thisThrows()); Ouch.
 Or I guess more accurately, is there any guarantee that the 
 assert in
 func() below should always pass?:

     class Foo {
         int i = 1;
         //...etc...
     }

     struct Bar {
         Foo foo;
         ~this() {
             foo.i = 2;
         }
         //...etc...
     }

     void func(Bar bar)
     {
         //...anything here that *doesn't* change bar.foo.i...

         assert(bar.foo.i == 1);  // Guaranteed to pass?
     }

     void main() {
         Foo f = new Foo();
         func(Bar(f));
     }
Here yes, but in general case if there are other arguments, and one if them passed by lambda invocation which touches f and modifies, then no.
Cool, thanks.
Jun 27 2013
next sibling parent reply "monarch_dodra" <monarchdodra gmail.com> writes:
On Friday, 28 June 2013 at 06:15:26 UTC, Nick Sabalausky wrote:
 On Fri, 28 Jun 2013 07:11:05 +0200
 "Maxim Fomin" <maxim maxim-fomin.ru> wrote:

 On Friday, 28 June 2013 at 04:54:56 UTC, Nick Sabalausky wrote:
 Probably a silly question, but I wanted to double-check...

 If you have this:

     struct Foo {...}
     bar(Foo());

 Then regardless of optimizations (aside from any optimizer 
 bugs, of
 course) the Foo temporary can't go out of scope or have its 
 dtor called
 until bar finishes executing, right?
Struct dtor is always called in the end of the caller (bar in example). This will be OK, but in general case no. Currently object is copied in caller side but destroyed in callee side, and if one of the arguments next to struct is passed by invoking function which throws, callee and respectively dtor will never be called.
Interesting. BTW, for anyone else reading, I just searched bugzilla and it looks Kinda ugly as it means refcounted RAII structs can leek under this condition: func(refCountedStruct, thisThrows()); Ouch.
Just in case it wasn't clear from the original explanation, this is a bug, it *should* be perfectly safe to pass as many temps as you want, and expect the right amount of destructor called in case of a throw.
Jun 28 2013
parent reply "Maxim Fomin" <maxim maxim-fomin.ru> writes:
On Friday, 28 June 2013 at 08:08:17 UTC, monarch_dodra wrote:
 Just in case it wasn't clear from the original explanation, 
 this is a bug, it *should* be perfectly safe to pass as many 
 temps as you want, and expect the right amount of destructor 
 called in case of a throw.
Original explanation lacks the word "bug" deliberately because this is not a bug (in a sense that dmd generates wrong code), but a language design problem. How could you do this: struct S { int i = 1; } void foo(S s) { s.i = 2; } void main() { S s; foo(s); } Currently there are two dtors, one which gets S(2) at the end of foo and second at the end of main, which gets S(1). If you move dtor from callee to caller, it would get S(1) object (struct is passed by value), but it doesn't make sense to destruct S(1) where you have S(2). One possible solution is to pass by pointer in low level, which would probably increase magnitude of problems.
Jun 28 2013
parent reply "monarch_dodra" <monarchdodra gmail.com> writes:
On Friday, 28 June 2013 at 14:26:04 UTC, Maxim Fomin wrote:
 On Friday, 28 June 2013 at 08:08:17 UTC, monarch_dodra wrote:
 Just in case it wasn't clear from the original explanation, 
 this is a bug, it *should* be perfectly safe to pass as many 
 temps as you want, and expect the right amount of destructor 
 called in case of a throw.
Original explanation lacks the word "bug" deliberately because this is not a bug (in a sense that dmd generates wrong code), but a language design problem. How could you do this: struct S { int i = 1; } void foo(S s) { s.i = 2; } void main() { S s; foo(s); } Currently there are two dtors, one which gets S(2) at the end of foo and second at the end of main, which gets S(1). If you move dtor from callee to caller, it would get S(1) object (struct is passed by value), but it doesn't make sense to destruct S(1) where you have S(2). One possible solution is to pass by pointer in low level, which would probably increase magnitude of problems.
I don't understand the problem... There *should* be two destroyers... "main.s" is postblitted into "foo.s", and then foo destroys "foo.s" at the end of its scope... Where is the problem here?
Jun 28 2013
parent reply "monarch_dodra" <monarchdodra gmail.com> writes:
On Friday, 28 June 2013 at 15:12:01 UTC, monarch_dodra wrote:
 On Friday, 28 June 2013 at 14:26:04 UTC, Maxim Fomin wrote:
 [...]
I don't understand the problem... There *should* be two destroyers... "main.s" is postblitted into "foo.s", and then foo destroys "foo.s" at the end of its scope... Where is the problem here?
-------- import std.stdio; struct S { int i = 0; this(int i){this.i = i; writeln("constructing: ", i);} this(this){writeln("postbliting: ", i);} ~this(){writeln("destroying: ", i);} } void foo(S s) { s.i = 2; } void main() { S s = S(1); foo(s); } -------- constructing: 1 postbliting: 1 destroying: 2 destroying: 1 -------- Should I have expected a different behavior?
Jun 28 2013
parent reply "Maxim Fomin" <maxim maxim-fomin.ru> writes:
On Friday, 28 June 2013 at 15:17:12 UTC, monarch_dodra wrote:
 Should I have expected a different behavior?
import std.stdio; int callme() { throw new Exception(""); } struct S { int i = 0; this(int i){this.i = i; writeln("constructing: ", i);} this(this){writeln("postbliting: ", i);} ~this(){writeln("destroying: ", i);} } void foo(S s, int i) { s.i = 2; } void main() { S s = S(1); foo(s, callme()); } Destructor for copied object is not called because it is placed in foo(). Before calling foo(), dmd makes a copy of main.s, calls postblit, then puts code to invoke callme() and code to invoke foo(). Since callme() throws, foo() is not called and destructor placed in foo() is also not called. A struct copy escapes destructor. Now, if you try fix this by putting dtor for copy not in foo(), but in main immediately after foo() invocation, you will have a problem because destructor would get S(1) object while it should destroy S(2). Any modification made in foo() is lost. This can be possible fixed by passing copy by reference which would probably create new ABI problems.
Jun 28 2013
parent reply "monarch_dodra" <monarchdodra gmail.com> writes:
On Friday, 28 June 2013 at 15:33:40 UTC, Maxim Fomin wrote:
 On Friday, 28 June 2013 at 15:17:12 UTC, monarch_dodra wrote:
 Should I have expected a different behavior?
import std.stdio; int callme() { throw new Exception(""); } struct S { int i = 0; this(int i){this.i = i; writeln("constructing: ", i);} this(this){writeln("postbliting: ", i);} ~this(){writeln("destroying: ", i);} } void foo(S s, int i) { s.i = 2; } void main() { S s = S(1); foo(s, callme()); } Destructor for copied object is not called because it is placed in foo(). Before calling foo(), dmd makes a copy of main.s, calls postblit, then puts code to invoke callme() and code to invoke foo(). Since callme() throws, foo() is not called and destructor placed in foo() is also not called. A struct copy escapes destructor. Now, if you try fix this by putting dtor for copy not in foo(), but in main immediately after foo() invocation, you will have a problem because destructor would get S(1) object while it should destroy S(2). Any modification made in foo() is lost. This can be possible fixed by passing copy by reference which would probably create new ABI problems.
I thought that was where you were getting to. Couldn't this simply be solved by having the *caller*, destroy the object that was postblitted into foo? Since foo ends up not being called (because of the exception), then I see no problem having the caller destroy the "to-be-passed-but-ends-up-not" object? Basically, it would mean creating a "argument scope" into which each arg is constructed. If something throws, then the "up to now built args" are deconstruted just like with standard scope. If you reach the end of the scope, then call is made, but passing the resposability of destruction to foo. EG, pseudo code: memcpy_all_args; try { foreach arg in args: arg.postblit; exit(failure) arg.destroy; } call_foo; Isn't that how C++ does it? In terms of passing args by value, I see no difference between CC and postblit... And I'm 99% sure C++ doesn't have this problem...
Jun 28 2013
next sibling parent reply =?UTF-8?B?QWxpIMOHZWhyZWxp?= <acehreli yahoo.com> writes:
On 06/28/2013 09:01 AM, monarch_dodra wrote:

 And I'm 99% sure C++ doesn't have this problem...
+1%. :) I just finished checking. No, C++ does not have this problem. But there is the following related issue, which every real C++ programmer should know. ;) http://www.boost.org/doc/libs/1_53_0/libs/smart_ptr/shared_ptr.htm#BestPractices Ali P.S. The C++ program that I have just used for testing: #include <iostream> #include <stdexcept> using namespace std; int callme() { throw runtime_error(""); return 0; } struct S { int i_; S(int i) : i_(i) { cout << "constructing: " << i_ << " at " << this << '\n'; } S(const S & that) { cout << "copying: " << that.i_ << " to " << this << '\n'; i_ = that.i_; } ~S() { cout << "destroying: " << i_ << " at " << this << '\n'; } }; void foo(int i, S s) { s.i_ = 2; } int main() { S s = S(1); try { foo(callme(), s); } catch (...) { cout << "caught\n"; } }
Jun 28 2013
next sibling parent reply "Steven Schveighoffer" <schveiguy yahoo.com> writes:
On Fri, 28 Jun 2013 12:44:02 -0400, Ali =C3=87ehreli <acehreli yahoo.com=
 wrote:
 I just finished checking. No, C++ does not have this problem. But ther=
e =
 is the following related issue, which every real C++ programmer should=
=
 know. ;)

   =
 http://www.boost.org/doc/libs/1_53_0/libs/smart_ptr/shared_ptr.htm#Bes=
tPractices Thank you, I didn't know this. I can consider myself a "real" C++ = programmer now :) -Steve
Jun 28 2013
parent =?UTF-8?B?QWxpIMOHZWhyZWxp?= <acehreli yahoo.com> writes:
On 06/28/2013 10:11 AM, Steven Schveighoffer wrote:

 On Fri, 28 Jun 2013 12:44:02 -0400, Ali Çehreli <acehreli yahoo.com> 
wrote:
 
http://www.boost.org/doc/libs/1_53_0/libs/smart_ptr/shared_p r.htm#BestPractices

 Thank you, I didn't know this.
Even though this issue is covered in Herb Sutter's Exceptional C++ book, which I had read with great interest, I re-learned it last year when a colleague showed that link to me. I have always considered C++ a language where mere-mortals like myself must read lots and lots of books to advance (or to write any decent code). This thread makes me think that D is following in C++'s steps. I am too normal to figure out these corner cases myself. :-/ Ali
Jun 28 2013
prev sibling parent reply "Maxim Fomin" <maxim maxim-fomin.ru> writes:
On Friday, 28 June 2013 at 16:44:03 UTC, Ali Çehreli wrote:
 Ali

 P.S. The C++ program that I have just used for testing:
Are you sure that the code is exact translation of demonstrated D problem? I see difference in argument passing order and your version uses try-catch block. This code #include <iostream> #include <stdexcept> using namespace std; int callme() { throw runtime_error(""); return 0; } struct S { int i_; S(int i) : i_(i) { cout << "constructing: " << i_ << " at " << this << '\n'; } S(const S & that) { cout << "copying: " << that.i_ << " to " << this << '\n'; i_ = that.i_; } ~S() { cout << "destroying: " << i_ << " at " << this << '\n'; } }; void foo(S s, int i) { s.i_ = 2; } int main() { S s = S(1); foo(s, callme()); } prints for me: constructing: 1 at 0x7fffb93078d0 terminate called after throwing an instance of 'std::runtime_error' what(): Aborted
Jun 28 2013
parent reply =?UTF-8?B?QWxpIMOHZWhyZWxp?= <acehreli yahoo.com> writes:
On 06/28/2013 10:17 AM, Maxim Fomin wrote:

 Are you sure that the code is exact translation of demonstrated D
 problem?
Sorry. I omitted two points.
 I see difference in argument passing order and your version
 uses try-catch block.
1) C++ does not specify whether the stack gets unwound when the program terminates with an uncaught exception. That's why I caught to ensure that the stack objects would be destroyed. 2) C++ does not specify in what order function arguments are evaluated. I swapped the parameters because I used gcc under Linux, where the parameters are executed from right-to-left. Ali
Jun 28 2013
parent reply "monarch_dodra" <monarchdodra gmail.com> writes:
On Friday, 28 June 2013 at 17:30:58 UTC, Ali Çehreli wrote:
 On 06/28/2013 10:17 AM, Maxim Fomin wrote:

 Are you sure that the code is exact translation of
demonstrated D
 problem?
Sorry. I omitted two points.
 I see difference in argument passing order and your version
 uses try-catch block.
1) C++ does not specify whether the stack gets unwound when the program terminates with an uncaught exception. That's why I caught to ensure that the stack objects would be destroyed.
Doesn't it? The stack needs to be unwound for the exception to even "escape". It's merely the globals that may not be destroyed. (AFAIK)
 2) C++ does not specify in what order function arguments are 
 evaluated. I swapped the parameters because I used gcc under 
 Linux, where the parameters are executed from right-to-left.

 Ali
Jun 28 2013
parent =?UTF-8?B?QWxpIMOHZWhyZWxp?= <acehreli yahoo.com> writes:
On 06/28/2013 10:53 AM, monarch_dodra wrote:

 On Friday, 28 June 2013 at 17:30:58 UTC, Ali Çehreli wrote:
 1) C++ does not specify whether the stack gets unwound when the
 program terminates with an uncaught exception. That's why I caught to
 ensure that the stack objects would be destroyed.
Doesn't it? The stack needs to be unwound for the exception to even "escape". It's merely the globals that may not be destroyed. (AFAIK)
The standard does not specify it. On Linux, abort() gets called causing a core dump, which may be more useful than executing the destructors. Ali
Jun 28 2013
prev sibling parent reply "Maxim Fomin" <maxim maxim-fomin.ru> writes:
On Friday, 28 June 2013 at 16:01:05 UTC, monarch_dodra wrote:
 I thought that was where you were getting to. Couldn't this 
 simply be solved by having the *caller*, destroy the object 
 that was postblitted into foo? Since foo ends up not being 
 called (because of the exception), then I see no problem having 
 the caller destroy the "to-be-passed-but-ends-up-not" object?
In case when there is no exception, struct argument is passed and is modified in callee, destructor in caller would have unchanged version (because structs are passed by value).
 Basically, it would mean creating a "argument scope" into which 
 each arg is constructed. If something throws, then the "up to 
 now built args" are deconstruted just like with standard scope. 
 If you reach the end of the scope, then call is made, but 
 passing the resposability of destruction to foo.
This is another option but suffers from the same problem (in posted examples exception is always thrown, but in reality it need not to).
Jun 28 2013
parent reply "monarch_dodra" <monarchdodra gmail.com> writes:
On Friday, 28 June 2013 at 16:50:07 UTC, Maxim Fomin wrote:
 On Friday, 28 June 2013 at 16:01:05 UTC, monarch_dodra wrote:
 I thought that was where you were getting to. Couldn't this 
 simply be solved by having the *caller*, destroy the object 
 that was postblitted into foo? Since foo ends up not being 
 called (because of the exception), then I see no problem 
 having the caller destroy the "to-be-passed-but-ends-up-not" 
 object?
In case when there is no exception, struct argument is passed and is modified in callee, destructor in caller would have unchanged version (because structs are passed by value).
I'm saying the "callee" destroys whatever is passed to it, all the time, but that means "callee" needs to actually be called. If "caller" constructs objects, but then fails to actually call "callee", then caller *has* to be responsible for destroying the objects it has built, but not passed to anyone. But this is only if an exception is thrown: No exception: Caller constructs objects into foo. foo is called, foo becomes owner of objects. foo finishes. foo destroys object. Exception: Caller starts construction. Exception is thrown. Caller destroys objects as exception is propagating. All objects are destroyed, exception goes up.
Jun 28 2013
parent "Maxim Fomin" <maxim maxim-fomin.ru> writes:
On Friday, 28 June 2013 at 16:57:29 UTC, monarch_dodra wrote:
 On Friday, 28 June 2013 at 16:50:07 UTC, Maxim Fomin wrote:
 On Friday, 28 June 2013 at 16:01:05 UTC, monarch_dodra wrote:
 I thought that was where you were getting to. Couldn't this 
 simply be solved by having the *caller*, destroy the object 
 that was postblitted into foo? Since foo ends up not being 
 called (because of the exception), then I see no problem 
 having the caller destroy the "to-be-passed-but-ends-up-not" 
 object?
In case when there is no exception, struct argument is passed and is modified in callee, destructor in caller would have unchanged version (because structs are passed by value).
I'm saying the "callee" destroys whatever is passed to it, all the time, but that means "callee" needs to actually be called. If "caller" constructs objects, but then fails to actually call "callee", then caller *has* to be responsible for destroying the objects it has built, but not passed to anyone. But this is only if an exception is thrown: No exception: Caller constructs objects into foo. foo is called, foo becomes owner of objects. foo finishes. foo destroys object. Exception: Caller starts construction. Exception is thrown. Caller destroys objects as exception is propagating. All objects are destroyed, exception goes up.
In this scenario caller should distinguish between two situations: exception was thrown during arguments evaluation or was thrown somewhere in foo, when callee can call destructor for passed argument. Otherwise (for example, using current scope(failure) mechanism to tackle the problem), destructor would be called twice on same struct - first time inside callee as usual, second time - in caller which would think that exception was thrown during argument evaluation. I think some tweak required here, but this sound like a good solution.
Jun 28 2013
prev sibling parent Marco Leise <Marco.Leise gmx.de> writes:
Am Fri, 28 Jun 2013 02:15:19 -0400
schrieb Nick Sabalausky <SeeWebsiteToContactMe semitwist.com>:

 BTW, for anyone else reading, I just searched bugzilla and it looks

Reminds me of an issue I reported later: http://d.puremagic.com/issues/show_bug.cgi?id=10409 -- Marco
Jun 28 2013