digitalmars.D - Move semantics, D vs. C++, ABI details
This is an attempt to clarify some of the recent confusion wrt. DIP 1014, Walter's statement that DMD wouldn't move structs etc. My understanding of the terminology: * D moving: copy bits to another memory location, skipping postblit for the moved-to object & skipping destruction of the moved-from object * C++ moving: moved-to object constructed via special constructor hijacking the moved-from object's data and resetting the moved-from object for safe destruction (no double-free etc.) More interesting are the following low-level ABI differences. No guarantees for completeness/absolute correctness from my side (but I've worked on LDC's ABI implementations). C++11: 1) Non-POD by-value arguments are passed by reference, low-level-wise, and never on the stack or in registers. 2) The argument/parameter is allocated on the caller's stack and destructed by the caller after the call. struct S { S(int) {} // ctor S(const S&) {} // copy ctor S(S&&) {} // move ctor ~S() {} // dtor }; void foo(S); void bar() { // 1) passing an rvalue foo(S(123)); /* => * S tmp(123); // construct temporary * foo(&tmp); // pass the temporary directly by ref, no move ctor involved * tmp.~S(); // destruct it after the call */ // 2) passing an lvalue S lval(456); foo(lval); /* => * S tmp(lval); // construct temporary via copy ctor * foo(&tmp); // pass the temporary directly by ref * tmp.~S(); // destruct it after the call */ // 3) using std::move foo(std::move(lval)); /* => * S tmp(std::move(lval)); // construct temporary via move ctor (possibly mutating lval) * foo(&tmp); // pass the temporary by ref * tmp.~S(); // destruct it after the call */ } ==================== D: 1) Non-POD by-value arguments are passed by value, i.e., on the stack [or in registers]. 2) The callee destructs the parameter; the caller doesn't perform any cleanup/destruction after the call. struct S { this(int) {} // ctor this(this) {} // postblit ~this() {} // dtor } void foo(S); void bar() { // 1) passing an rvalue foo(S(123)); /* => * S tmp = S(123); // construct temporary * foo(tmp); // pass the temporary by value, i.e., move * // to foo params stack (copy bits, no postblit call) * // foo() will destruct its param (moved-to object) * // destruction of tmp is skipped (no need to reset to S.init) */ // 2) passing an lvalue S lval = S(456); foo(lval); /* => * S tmp = lval; // construct temporary by bitcopy + postblit call * foo(tmp); // pass the temporary by value, see rvalue case * // foo() will destruct its param & tmp's dtor is disabled (in the AST) */ } D's rvalue case (explicit temporary + move to params stack) is how LDC handles it to satisfy LLVM IR (before optimization); other compilers might emplace the argument directly in the parameters stack. For DIP 1014, we (at least LDC) would most likely need to adopt the C++ ABI in this regard, i.e., always pass non-PODs by reference - AFAIK, the LLVM IR doesn't provide the means to get the final address of the (moved-to) parameter in the callee's parameter stack, inside the caller's scope (required for the proposed postmove call), with whatever that entails (don't disable dtor for lvalue copies in AST, destruct the temporaries after the call in a finally block if the callee potentially throws etc.).
Oct 03 2018
On Wednesday, 3 October 2018 at 20:57:39 UTC, kinke wrote:For DIP 1014, we (at least LDC) would most likely need to adopt the C++ ABI in this regard, i.e., always pass non-PODs by referenceWhich would also help with C++ interop of course - while LDC's extern(C++) ABI was fixed wrt. passing all non-PODs by reference, the different destruction rules are still an issue (no destruction when calling a C++ function from D, and double destruction when calling an extern(C++) D function from C++).
Oct 03 2018