I had a bad time with slice-in-struct array operation forwarding/mimicing. What's the best way to do it?

Adam D. Ruppe destructionator at gmail.com
Sat May 4 16:10:36 UTC 2019


On Saturday, 4 May 2019 at 15:18:58 UTC, Random D user wrote:
> But array copy and setting/clearing doesn't:
> int[] bar = [ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 
> 15 ];
> foo[] = bar[];
>
> And I get this very cryptic message:
> (6): Error: template `example.Array2D!int.Array2D.opSlice` 
> cannot deduce function from argument types `!()()`, candidates 
> are:
> (51):        `example.Array2D!int.Array2D.opSlice(ulong 
> dim)(int start, int end) if (dim >= 0 && (dim < 2))`
>
> 1. WTF `!()()` and I haven't even called anything with opSlice



This comes from some old history: arr[] used to call opSlice(), 
and now it is preferred to implement opIndex() instead, but the 
compiler still supports the old zero-arg opSlice() too.

Since opIndex() didn't work, it moved to trying the older 
opSlice(), and that didn't work leading it to give up and issue 
the error.

But yeah, the error should probably mention the newer function 
name instead of the fallback it is failing on...

> Next I added a ref to the E[] opIndex():
> ref E[] opIndex() { return impl; }

I would avoid ref as much as you can, for one because it 
conflates getting and assigning (as you saw), but also because it 
leaks your internal implementation detail to the user; if you 
didn't use an array internally, that api would break.

> So I added:
> ref E[] opIndexAssign(E value) { impl[] = value; return impl; }
>
> And now foo[] = 0; works, but foo[0, 1] = foo[2, 3] doesn't.
> *snip*
> 4. WTF. So basically adding opIndexAssign(E value) disabled ref 
> E opIndex(int i, int j). Shouldn't it consider both?

Once you implement an opIndexAssign - any opIndexAssign - all 
uses of `a[...] = c` will go through it instead of normal opIndex.

Generally speaking, opIndex is for getting, opIndexAssign is for 
setting. Sometimes setting can be done via a getter function 
(like if ref), but they are mostly two different functions and 
you should implement them both if you want read/write access.

This is especially important if your underlying data isn't 
actually in a literal array.

> foo[] = bar; // Full copy

implement:

opIndexAssign(typeof(this) rhs);

In opIndexAssign, generally, the arguments are 
(value_on_right_hand_side, indexes...)

Since there was no index given here, you don't want an index 
argument.

> foo[] = 0; // Full clear

implement:

opIndexAssign(int rhs);

Now it will take any int for the whole thing.

> foo[0 .. 5, 1] = bar[ 0 .. 5]; // Row/Col copy

This is translated to:

this.opIndexAssign(bar.opIndex(bar.opSlice!0(0, 5)), 
this.opSlice!0(0, 5), 1)

Three different functions there. On the left, we see x[...] = y, 
so we know that is opIndexAssign again. The first argument is 
what it is set to, other arguments are the slices given.

Any x .. y is translated to opSlice!dim(low, high). The !0 in 
there is because it was given as the zeroth (first) argument in 
the slice.

In this example, `foo[1 .. 2, 3 .. 4]`, it would call

opIndex( opSlice!0(1, 2), opSlice!1(3, 4) )

With the !0 and !1 indicating which position the slice was in.

The implementation of these functions would depend on just what 
your innards are... but the types might be something like this:

struct SliceHelper {
    size_t start;
    size_t end;
    int stride;
}

SliceHelper opSlice(size_t dimension)(size_t start, size_t end) {
     /* return the helper with the appropriate values */
}

now, we can implement opIndex for getting in terms of that and 
some regular items:

// get a single item at point x, y
int opIndex(size_t x, size_t y) { }

// well not necessarily an array, maybe a range for lazy 
processing, but meh you get the idea
// this gives a slice in the X dimension with a fixed Y coordinate
// e.g. foo[ 0 .. 5, 3]
int[] opIndex(SliceHelper x, size_t y) {}

// foo[0, 4 .. 6]
int[] opIndex(size_t x, SliceHelper y) {}

// and now a 2d section of it
Array2d!int opIndex(SliceHelper x, SliceHelper y) { }

// and the zero-arg version, for foo[]
// here I return this for an example, but by convention,
// this should actually return a range object that is a
// view into this container... which might be `this` but
// might not be, depending on the details of your code like
// if it has internal references or other stuff you don't want
// to leak to the outside.

typeof(this) opIndex() { return this; }


And then a similar combination of arguments for opIndexAssign
(I return void here but it is also common to return this; )

void opIndexAssign(int rhs) // this[] = rhs
void opIndexAssign(int rhs, size_t x, size_t y) // this[x, y] = 
rhs
void opIndexAssign(int rhs, size_t x, SliceHelper y) // this[x, 
y1 .. y2] = rhs


.... you get the idea. The overload for other types of rhs.

This might be dozens of functions! You can minimize that a bit by 
templating them and using internal static if, loops, etc to 
narrow it down.


void opIndexAssign(R, Idx1, Idx2)(R rhs, Idx1 x, Idx2 y) {
     // test types of Idx1+Idx2 for figuring out what to change
     // test type of R to see what to change it to
}


> foo[1, 0 .. 5] = 0; // Row/Col clear

Again, notice the x[...] = y, so we know it is opIndexAssign.


opIndexAssign(0 /* rhs */, 1, opSlice!1(0, 5))

> foo[0 .. 5, 2 .. 4] = bar[ 1 .. 6, 0 .. 2 ]; // Box copy

opIndexAssign( // opIndexAssign as a setter
    bar.opIndex(bar.opSlice!0(1, 6), bar.opSlice!1(0, 2)), // rhs 
value, notice opIndex as a getter
    opSlice!0(0, 5), opSlice!1(2, 4) /* lhs slice args */)

> foo[0 .. 5, 2 .. 4] = 0; // Box clear

foo.opIndexAssign( // setter
    0, // rhs
    foo.opSlice!0(0, 5), foo.opSlice!1(2, 4) // lhs slice args
)


> I suppose I can manually define every case one by one and not 
> return/use any references etc.


Yeah, it can be quite a lot of combinations of arguments. 
Generally, it is n^n * k, where n is the number of dimensions of 
your array and k is the number of different types you want to be 
able to assign to it. Then times two for getters and setters, 
then plus two for the empty arg ones.

For 1d with one type, it is easy: 1^1 * 1 * 2 + 2:

the 1^1 * 1 is simplified: int func(int), and *2 is Index and 
IndexAssign:

opIndex(int) and opIndexAssign(int, int)

then the plus two

opIndex() and opIndexAssign(int) for foo[] and foo[] = n.


OK, maybe actually *3 instead of *2 if you want to do

opOpIndexAssign as well, to enable like

foo[] += 4;

that's separate too.


But for 2d and 3d and more arrays, the number of functions 
explodes really fast. You will probably want to template that 
into something generic and just implement it that way.


More information about the Digitalmars-d-learn mailing list