Move semantics, D vs. C++, ABI details
kinke
noone at nowhere.com
Wed Oct 3 20:57:39 UTC 2018
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.).
More information about the Digitalmars-d
mailing list