A possible solution for the opIndexXxxAssign morass

Bill Baxter wbaxter at gmail.com
Wed Oct 14 09:34:36 PDT 2009


On Wed, Oct 14, 2009 at 7:42 AM, Jason House
<jason.james.house at gmail.com> wrote:
> Andrei Alexandrescu Wrote:
>
>> Right now we're in trouble with operators: opIndex and opIndexAssign
>> don't seem to be up to snuff because they don't catch operations like
>>
>> a[b] += c;
>>
>> with reasonable expressiveness and efficiency.
>
> I would hope that *= += /= and friends could all be handled efficiently with one function written by the programmer. As I see it, there are 3 basic steps:
> 1. Look up a value by index
> 2. Mutate the value
> 3. Store the result

And as Chad J reminds us, same goes for in-place property mutations
like  a.b += c.
It's just a matter of  accessing  .b  vs .opIndex(b).   And really
same goes for any function  a.memfun(b) += c could benefit from the
same thing (a.length(index)+=3 anyone?)

> it's possible to use opIndex for #1 and opIndexAssign for #3, but that's not efficient. #1 and #3 should be part of the same function, but I think #2 shouldnot be. What about defining an opIndexOpOpAssign that accepts a delegate for #2 and then use compiler magic to specialize/inline it?

It could also be done using a template thing to inject the "mutate the
value" operation:

void opIndexOpOpAssignOpSpamOpSpamSpamSpam(string Op)(Thang c, Thing idx) {
     ref v = <lookup [idx] however you like>
     mixin("v "~Op~" c;");
     <store to v to [idx] however you like>
}

or make it an alias function argument and use Op(v, b).

Sparse matrices are a good case to look at for issues.  a[b] is
defined for every [b], but if the value is zero nothing is actually
stored.  So there may or may not be something you can return a
reference to.   In C++ things like std::map just declare that if you
try to access a value that isn't there, it gets created.  That way
operator[] can always return a reference.   It would be great if we
could make a[b] not force a ref return in cases where there is no
lvalue that corresponds to the index (or property) being accessed.
Gracefully degrade to the slow path in those cases.

A good thing about a template is you can pretty easily specify which
cases to allow using template constraints:

void opIndexOpOpAssignOpSpamOpSpamSpamSpam(string Op)(Thang c, Thing b)
       if (Op in "+= -=")
{
   ...
}

(+ 1 small dream there about 'in' being defined to mean substring
search for string arguments -- that doesn't currently work does it?)

If the template can't be instantiated for the particular operation,
then the compiler would try to revert to the less efficient standby:
auto tmp = a[b];
tmp op= c;
a[b] = tmp;

The whole thing can generalize to all accessors too.  Instead of just
passing the Op, the compiler could pass the accessor string, and args
for that accessor.  Here an accessor means ".opIndex(b)",  ".foo", or
even a general ".memfun(b)"


void opIndexOpOpAssignOpSpamOpSpamSpamSpam(string Member, string
Op)(Thang c, Thing b)
   if (Member in ".foo() .bar() .opIndex()")
{
     string call = ctReplace(Member, "()", "(b)");  // Member looks
like ".memfun()"  this turns it into ".memfun(b)"
     ref v = mixin("this" ~ call ~ ";");
     < any extra stuff you want to do on accesses to v >
     mixin("v "~Op~" c;");
     < store v back to member >
}

It's ugly and perhaps too low-level, but that can be worked on if the
general principle is sound.   Utility functions can be defined to do
whatever it is that turns out to be a recurring pattern.  Lack of
being virtual could be a problem for classes.

--bb



More information about the Digitalmars-d mailing list