On Wed, 06 Oct 2010 12:34:54 -0400, Andrei Alexandrescu <seewebsiteforem...@erdani.org> wrote:

I think ranges are a great thing, having simplicity as one of their advantages. In the beginning they were indeed rather simple:

struct MyRange {
     bool empty();
     ref Widget front();
     void popFront();
}

with the appropriate refinements for bidirectional and random.

Then there was the need to distinguish one-pass, "input" ranges (e.g. files) from many-pass, "forward" ranges. So the "save" function got born for forward ranges and above:

struct MyRange {
     bool empty();
     ref Widget front();
     void popFront();
     MyRange save();
}

Then @property came about which required extra adornments:

struct MyRange {
     @property bool empty();
     @property ref Widget front();
     void popFront();
     @property MyRange save();
}

or more succinctly:

struct MyRange {
    @property {
       bool empty();
       ref Widget front();
       MyRange save();
    }
    void popFront();
}


Then some ranges were unable to return ref, but they could receive assignment. I call these /sealed ranges/ because they "seal" the address of their elements making it inaccessible:

struct MyRange {
     @property bool empty();
     @property Widget front();
     @property void front(Widget);
     void popFront();
     @property MyRange save();
}

This made bidirectional and random-access range interfaces quite big because now we're talking about back() (two versions), opIndex(), opIndexAssign() and opIndexOpAssign().

Until now I think we're solid on design decisions. The discussion starts here.

I agree all of this except for save() (not a secret to anyone who's read my posts).

WRT sealed ranges, that is one of those things where you have a tradeoff between simplicity and control. Note that with sealed ranges, they are used almost exactly like unsealed ranges except for a couple restrictions. This is important for users because they can be considered hidden details (see my note later).

And then there was this nagging thing which is well-understood in the C++ world. A sealed range returns by value and accepts by value. But in C++, an object's copy constructor is arbitrarily expensive. The library must be designed in accordance to that and carefully navigate around that issue.

[snip]

To solve that problem, I introduced moveFront(), moveBack(), and moveAt(size_t), all of which destructively read the front, back, or an indexed element respectively off the range. Then you can swap r1.front with r2.front at constant cost like this:

     T tmp = r1.moveFront();
     r1.front = r2.moveFront();
     r2.front = move(tmp);

I'll also note, another unsolved issue with this is iteration -- iterating a sealed range via foreach is not going to use moveFront.

All of this works and is rock-solid, but it does load the range interface considerably. To a newcomer coming without the background above, a full-fledged range definition may look quite loaded.

One simplification is to simply decree that Phobos (and D in general) shuns objects with eager copy. Any this(this) could be considered constant cost.

Yes, I like this plan. I recently ran into this problem and just to simplify things unsealed my range in question rather than define moveX.

I'll note that moveX is more trouble than simply sealing ranges by not returning ref because the usage changes -- you have to start using moveX if its defined, or just X when it's not. And if you don't, there is no warning or error from the compiler.

2. It would force certain types (such as BigInt) that allocate resources and have value semantics to resort to reference counting.

Or to be considered immutable. I believe Java does it this way. Of course, Java's GC is much better at dealing with this...

4. It would be a definite departure from C++, where all value copies are considered of arbitrary cost. This would provide a convenient straw-man for naysayers (e.g. "Hey, D calls the copy constructor even more often than C++! No thanks, I'll stick with C++0x which solves it all with rvalue references").

Wait, shouldn't auto ref solve the rvalue references thing? I mean if you are copying an rvalue, there is no reason to keep the original around, so no need to call the copy constructor, right?

-Steve

Reply via email to