This is related to a discussion[1] that I had started recently but I will give an even shorter example here:

void main()
{
    // Two slices to all element
    auto a = [ 1, 2, 3, 4 ];
    auto b = a;

    // Initially, they both have capacity (strange! :) )
    assert(a.capacity == 7);
    assert(b.capacity == 7);

    // The first one that gets a new element gets the capacity
    b ~= 42;
    assert(a.capacity == 0);    // <-- a loses
    assert(b.capacity == 7);
}

The interesting thing is that this situation is the same as appending to a slice parameter:

void foo(int[] b)
{
    // Since stomping is prevented by the runtime, I am
    // foolishly assuming that I can freely append to my
    // parameter. Unfortunately, this action will cost the
    // original slice its capacity.
    b ~= 42;
}

void main()
{
    auto a = [ 1, 2, 3, 4 ];

    assert(a.capacity == 7);
    foo(a);
    assert(a.capacity == 0);    // <-- Capacity is gone :(
}

Note that the code above is about a function appending to a parameter for its own implementation. Otherwise, the appended element cannot be seen by the original slice anyway.

Also note that it would be the same if the parameter were const(int)[].

This is a new discovery of mine. Note that this is different from the non-determinism of when two slices stop sharing elements.[2] To me, this is very a strange consequence of passing a slice /by value/ despite the common expectation that by value leaves the original variable untouched. After this, I am tempted to come up with the following guideline.

(I am leaving 'immutable' and 'shared' out of this discussion.)

Guideline: Slice parameters should either be passed by reference to non-const or passed by value to const:

  1a) void foo(ref       int [] arr);  // can modify everything

  1b) void foo(ref const(int)[] arr);  // cannot modify elements

  2)  void foo(    const(int[]) arr);  // cannot affect anything
                                       // (even capacity)

Only then the caller can be sure that the capacity of the original slice will not change. (I am assuming that the function is smart enough not to call assumeUnique.)

Since 1a and 1b are by reference, the function would not append to the parameter for its own implementation purposes anyway. If it did append, it would be with the intention of that particular side-effect.

For 2, thanks to the const parameter, the function must copy the slice first before appending to it.

If the parameter is to a mutable slice (even with const elements) then the original slice can lose capacity. It is possible to come up with solutions that preserve the capacity of the original slice but I think the previous guideline is sufficient:

    foo(a.capacityPreserved);

(capacityPreserved can be a function, returning a RAII object, which calls assumeUnique in its destructor on the original slice.)

Does the guideline make sense?

Ali

[1] http://forum.dlang.org/thread/[email protected]

[2] http://dlang.org/d-array-article.html

Reply via email to