Search code examples
rangedpointer-arithmetic

Why do th array range primitives consume their sources?


The range primitives that are dedicated to the built-in arrays consume their sources but one could easily design a range system that would rather be based on the .ptr of the source (at first look that's more flexible).

struct ArrayRange(T)
{
    private T* _front;
    private T* _back;

    this(ref T[] stuff) {
        _front = stuff.ptr; 
        _back = _front + stuff.length;
    } 
    @property bool empty(){return _front == _back;}  
    @property T front(){ return *_front;}  
    @property T back(){return *_back;}    
    void popFront(){ ++_front;}
    void popBack(){--_back;}
    T[] array(){return _front[0 .. _back - _front];}
    typeof(this) save() {
        auto r = this.array.dup;
        return typeof(this)(r); 
    }  
}

void main(string[] args)
{
    auto s = "bla".dup;
    // here the source is 'fire-proofed'
    auto rng = ArrayRange!char(s);   
    rng.popFront;
    assert (s == "bla");
    assert (rng.array == "la");
    // default primitives: now the source is consumed 
    import std.array;
    s.popFront;
    assert (s == "la");
}

Why is the default system not based on pointer arithmetic since poping the front imply reallocations/less efficiency?

Any rationale for this design?


Solution

  • I agree with you, there is no reason to reallocate at each popFront. Good thing that's not what happens then!

    The popFront mechanism is quite alike what you presented, and the source isn't consumed, only the array on which you call popFront (because, it is a pop after all). What you implemented is what happens when you slice an array: you get a reference range to the original array:

    auto a = [1, 2, 3]; 
    auto s = a[];
    s.popFront;
    assert(s == [2, 3]);
    assert(a == [1, 2, 3]);
    

    .dup is there to provide a copy of an array so that you can safely modify the copy without changing the original, so it copies the original array then provides an input range to this copy. Of course you can modify the copy (that's the point), and popFront will change it but still uses pointer arithmetic and without changing the source.

    auto a = [1, 2, 3];
    auto s = a.dup;
    s.popFront;
    assert(s = [2, 3]);
    assert(a = [1, 2, 3]);
    

    .dup may not seem very useful as such because we are using it with arrays, but it is really important when dealing with "pure" ranges as a range is often lazy not to consume it. As the copy of the range is consumed and not the initial one, we can safely pass this copy to a function and still have our initial lazy range intact.