Is there a way to enforce a sink argument of a procedure, so that the compiler will error if it needs to produce a copy, without disabling the copy hook? Much like r-value reference arguments in c++
I have only been able to make the compiler help me by disabling the copy hook in the type, but i need my type to be copyable, i only need to enforce the move in particular sink functions in the hot path. Tried also to add an explicit system.move, but the compiler seems it is not able to report use after move errors...
I have only been able to make the compiler help me by disabling the copy hook in the type, but i need my type to be copyable, i only need to enforce the move in particular sink functions in the hot path.
Maybe you can use a wrapper type for the hot path which has an overriden = that is disabled.
Maybe you can use a wrapper type for the hot path which has an overriden = that is disabled.
Sounds like a nice workaround for now, thanks for the advice !
But how to offer the feature otherwise... I don't know. An RFC is welcome.
So before i start writing an RFC, i would like to present my understanding (and proposal) so you can smash me here before i start writing a silly RFC :D
Consider this trivial example:
type
X = object
s: string
proc `=copy`(x: var X, y: X): void =
x.s = "copy of " & y.s
proc consume(x: sink X) =
echo "Consumed: ", x.s
var x = X(s: "abcdefg")
consume(move(x))
echo "Unconsumed: ", x.s # < Could the compiler hint i'm using after move ?
This prints this:
Consumed: abcdefg
Unconsumed:
But i would like to enable a compiler hint, if possible
Btw i'm still trying to understand how the sink works (sorry i'm pretty new to the language, coming from C++). Consider this slight variation:
type
X = object
s: string
proc `=copy`(x: var X, y: X) =
x.s = "copy of " & y.s
proc `=sink`(x: var X, y: X) =
`=destroy`(x)
wasMoved(x)
x.s = "moved " & y.s
proc consume(x: sink X) =
echo "Consumed: ", x.s
var x = X(s: "abcdefg")
consume(x)
This surprisingly prints:
Consumed: copy of abcdefg
the compiler seems unable to prove x is never read after the call to the proc with sinked argument. To confirm this, i tried this other snippet, and it doesn't work like i would expect:
type
X = object
s: string
proc `=copy`(x: var X, y: X) {.error.}
proc `=sink`(x: var X, y: X) =
`=destroy`(x)
wasMoved(x)
x.s = "moved " & y.s
proc consume(x: sink X) =
echo "Consumed: ", x.s
var x = X(s: "abcdefg")
consume(x)
Produces:
Error: '=copy' is not available for type <X>; requires a copy because it's not the last read of 'x'; routine: test
here we go ! https://github.com/nim-lang/RFCs/issues/432
bear with me this is my first RFC so i'll appreciate any feedback to improve wording or making things clearer
bear with me this is my first RFC so i'll appreciate any feedback to improve wording or making things clearer
It's a bit premature, but well written, thanks.
Regarding to how sink works:
A sink annotated parameter a basically tells the compiler that, inside the procedure, you're planning to - in C++ terms - move a somewhere. This allows you to have only one =sink invocation with no intermediate temporaries at the 'final' move location even when passing a value down through multiple procedure calls (sink needs to be present on the respective parameters though). You're not required to actually move a on all code paths or even at all, the compiler just makes sure that you can do so without disturbing anything at the callsite by creating a temporary there if necessary.
Note that sink is, strictly speaking, not merely a hint since:
proc a(x : var int, y : int) =
x = move y
does not compile, while:
proc b(x : var int, y : sink int) =
x = move y
does.
The following Nim code:
type
A = object
x : int
B = object
a : A
proc set(b : var B, a : sink A) =
b.a = a # Compiler infers `=sink` (move) here. No need to use `move`
# b.a = move a
proc test1() =
var a = A(x : 0)
var b : B
b.set(a)
proc test2() =
var a = A(x : 0)
var b : B
# A copy of `a` gets passed to `set`
b.set(a)
# modify a
a.x = 1
proc test3() =
let a = A(x : 0) # Using let here (roughly the same as const in C++)
var b : B
# No copy is created here. `a` gets moved from even though it's a let (constant) since `a` is last used here
b.set(a)
directly translated to C++20 (while keeping the same abstraction level):
struct A {
int x;
};
struct B {
A a;
};
void set(B& b, A& a) {
b.a = std::move(a);
}
void test1() {
A a{.a = 0};
B b;
set(b, a);
}
void test2() {
A a{.a = 0};
B b;
{
A temp = a; // Could also use the copy-constructor here instead of copy-assignment
set(b, temp);
}
a.x = 1;
}
void test3() {
// const A a{.x = 0};
// No equivalent since you can't bind a const l-value to a l-value reference. You'd have to either make `a` non-const or introduce a copy
}
sink T is translated to T (which is then either T or const T& in C++ land), you don't pay a for double indirection for sink ref T... It's awesome. :-)
Semantically, sink T is closest to a C++ by-copy parameter.
Ok, so I was wrong with my explanation of sink. I was under the impression that the code:
proc a(x : var X, y : sink Y) =
x.v = y
proc b(x : var X, y : sink Y) =
a(x, y)
proc test() =
var y = Y()
var x : X
b(x, y)
would compile to the following (shortened for readability) C code:
void a(X* x, Y* y) {
eqsink(x.v, y);
}
void b(X* x, Y* y) {
a(x, y);
}
void test() {
Y y;
X x;
// ...
b(&x, &y);
// ...
}
While in reality this code gets generated:
void a(X* x, Y* y) {
eqsink(x.v, y);
}
void b(X* x, Y* y) {
Y blitTmp;
blitTmp = *y;
a(x, &blitTmp);
}
void test() {
Y y;
X x;
Y blitTmp;
// ...
blitTmp = y;
b(&x, &blitTmp);
// ...
}
I did some testing and from what I gathered, it seems like gcc is, in some very simple cases, able to optimize the blit temporaries aways and do a direct copy. In more complex cases however this seems to not be the case. Example:
type
Y = object
s : string
a : array[30, int]
X = object
v : X
# using `a` and `b` function from sample above
proc test() =
var y = Y(s : "abcd")
var x : X
b(x, y)
The gcc optimizer fails to elide the blit temporaries in this case, which results in two extra unnecessary memset/memcpy pairs.
Compiler info:
Command line: nim c --gc:arc -d:release
In more complex cases however this seems to not be the case.
Please ensure that the compile does not use -fno-strict-aliasing which is still the default for -d:release but not required anymore, esp not for --gc:arc.