Bug 19904 – move semantics fail through the `emplace` pipeline
Status
RESOLVED
Resolution
FIXED
Severity
major
Priority
P1
Component
druntime
Product
D
Version
D2
Platform
All
OS
All
Creation time
2019-05-26T17:54:58Z
Last change time
2021-08-04T20:53:22Z
Assigned to
No Owner
Creator
Manu
Comments
Comment #0 by turkeyman — 2019-05-26T17:54:58Z
Consider the emplace function:
T* emplace(T, Args...)(T* chunk, auto ref Args args)
if (is(T == struct) || Args.length == 1)
{
import core.internal.lifetime : emplaceRef;
emplaceRef!T(*chunk, args);
return chunk;
}
Note there is no `forward!args` then it is passed to `emplaceRef`. This causes the arguments to be unnecessarily copied, potentially executing a bunch of copy code.
Now looking into `emplaceRef` (which I won't paste here because it's a monster!), you'll see many calls to:
p.__ctor(args);
Note a similar lack of `forward!args` at all the constructor calls, which leads to a second layer of unnecessary copying.
TL;DR, move semantics in D are woefully broken!
Comment #1 by kinke — 2019-05-27T20:04:27Z
(In reply to Manu from comment #0)
> Note there is no `forward!args` then it is passed to `emplaceRef`. This
> causes the arguments to be unnecessarily copied, potentially executing a
> bunch of copy code.
No copy code should be executed. The `auto ref` param will be a ref for an lvalue arg, and a moved bitcopy for an rvalue arg (D move semantics - arg isn't destructed and param's postblit is skipped, but a memcpy is performed).
emplaceRef uses `auto ref Args` too. So when calling it in emplace() with its (lvalue) parameters as arguments, they are passed by reference.
Comment #2 by kinke — 2019-05-27T20:24:14Z
This is what I mean:
import core.stdc.stdio;
struct S
{
this(this) { printf("postblit\n"); }
~this() { printf("dtor\n"); }
}
void foo(S s) { printf("rvalue\n"); }
void foo(ref S s) { printf("lvalue\n"); }
void forward(Args...)(auto ref Args args) { foo(args); }
void main()
{
printf("directly:\n");
foo(S());
printf("forward:\n");
forward(S());
}
=>
directly:
rvalue
dtor
forward:
lvalue
dtor
The rvalue-ness is lost when forwarding `auto ref` params this way (but still detectible via isRef trait).
Comment #3 by kinke — 2019-05-27T20:37:39Z
Ah, due to this rval->lval conversion when calling an `auto ref` function, the ctor is called with an lvalue arg, even if the emplace-arg was originally an rvalue, and that then leads to a superfluous copy (and later destruction) instead of a move at that end of the chain.
Comment #4 by turkeyman — 2019-05-27T21:35:38Z
Right, you unwrapped my example. You need another layer to see it all go wrong.
Comment #5 by andrei — 2019-05-27T22:07:06Z
First, we have a problem with identifying the ref vs the non-ref instantiation. This should compile and doesn't:
void fun(T)(auto ref T x) {
static if (is(x == ref)) { ... }
}
This does compile, but prints the wrong thing (non ref for both cases):
import std.stdio;
void fun(T)(auto ref T x) {
pragma(msg, __PRETTY_FUNCTION__);
static if (is(__traits(isRef, x))) { writeln("ref: ", x); }
else { writeln("non ref: ", x); }
}
void main() {
int a;
fun(a);
fun(42);
}
Should definitely be its own bug, so I just submitted https://issues.dlang.org/show_bug.cgi?id=19906.
This doesn't compile:
void fun(T)(auto ref T x) {
pragma(msg, __PRETTY_FUNCTION__);
static if (__traits(getParameterStorageClasses, fun, 0)[0] == "ref") { writeln("ref: ", x); }
else { writeln("non ref: ", x); }
}
The error message suggests you can't get access to the function name from within the function definition. This doesn't same to compile for the same reason:
void fun(T)(auto ref T x) {
import std.traits;
pragma(msg, __PRETTY_FUNCTION__);
static if (ParameterStorageClassTuple!fun[0] == ParameterStorageClass.ref_) { writeln("ref: ", x); }
else { writeln("non ref: ", x); }
}
Detecting ref-ness should be easily doable from within the instantiation. That's the first problem to tackle.
Comment #6 by atila.neves — 2019-05-28T08:09:44Z
This compiles and works as intended though:
import std.stdio;
void fun(T)(auto ref T x) {
static if(__traits(isRef, x))
writeln("x: ", x, " is a ref");
else
writeln("x: ", x, " is NOT a ref");
}
void main() {
fun(42);
int i;
fun(i);
}
Comment #7 by andrei — 2019-05-28T13:46:09Z
Yes, my bad... copy and paste strikes again!
Comment #8 by kinke — 2021-02-18T08:14:49Z
I completely forgot about this; it should be fixed now. The struct case was already handled, the class case was recently fixed with https://github.com/dlang/druntime/pull/3352.