3

Consider the following simplified example of a class holding a shared resource:

class array
{
    typedef std::array<float, 1000> resource;

public:
    // default constructor, creates resource
    array() : p_(std::make_shared<resource>()), a_(0), b_(1000) {}

    // create view
    array operator()(int a, int b) { return array(p_, a_+a, a_+b); }

    // get element
    float& operator[](int i) { return (*p_)[a_+i]; }

private:
    std::shared_ptr<resource> p_;
    int a_, b_;

    // constructor for views
    array(std::shared_ptr<resource> p, int a, int b) : p_(p), a_(a), b_(b) {}
};

Now I'm wondering how to define a semantics for this class that doesn't confuse its users. For example, I'd like to allow operator=() to copy elements:

array x, y;
x = y;                 // copies all elements from y's storage to x's
x(30,40) = y(50,60);   // copies 10 elements

But then, to be consistent, shouldn't the copy constructor and the copy assignment operator always copy? What about:

array z = x(80,90);    // will create an array referencing x's storage

In this case, the copy will be elided by the compiler, so no matter what my copy assignment operator does, z will hold a reference to x's storage. There's no way around this.

So does it make more sense for assignment to always create a reference, and copying to be declared explicitly? For example, I could define a wrapper class copy_wrapper, assignment of which forces copying of elements:

class copy_wrapper
{
    array& a_;
public:
    explicit copy_wrapper(array& a) : a_(a) {}
    array& get() { return a_; }
};

class array
{
    // ...
    array& operator=(const array& a);    // references
    array& operator=(copy_wrapper c);    // copies
    copy_wrapper copy() { return copy_wrapper(*this); }
};

This would force users to write:

array x, y;
x(30,40) = y(50,60).copy();  // ok, copies
x(30,40) = y(50,60);         // should this throw a runtime error?
x = y;                       // surprise! x's resource is destructed.

A bit cumbersome, and worse than that: not what you expect.

How should I deal with this ambiguity?

marton78
  • 3,899
  • 2
  • 27
  • 38
  • In `array z = x`, it's actually the copy constructor which gets called, not the assignment operator. The copy constructor is what you should implement. – avakar Oct 17 '12 at 08:34
  • Also, you could use Python list semantics: `z=x` copies a reference, `z(1,10)=x(1,10)` copies a range, `z() = x()` copies everything. – avakar Oct 17 '12 at 08:35
  • Right, bad example. The assignment `array z = x;` is not eligible for copy elision, since `z` is not a temporary. I have updated the question accordingly. – marton78 Oct 17 '12 at 09:09
  • again, the copy constructor will be called in `array z = x(80,90)`. – avakar Oct 17 '12 at 09:15
  • Generally, yes. But it depends. See [this answer](http://stackoverflow.com/a/1051468/728847). In any case, I'd like to implement the copy constructor and copy assignment operator in a way that they behave consistently, according the principle of least surprise. – marton78 Oct 17 '12 at 09:19
  • @avakar No, it can be elided. – R. Martinho Fernandes Oct 17 '12 at 09:19
  • I'd suggest introducing the notion of `sub_array`, or `array_ref` or something like that. – R. Martinho Fernandes Oct 17 '12 at 09:20
  • Yes, I thought about that. But I'd like the `subarray` to live on when the original `array` goes out of scope. So the whole discussion applies to `subarray`s then, just replace all occurrences of `array` above with `subarray`. Or am I overseeing something? – marton78 Oct 17 '12 at 09:23
  • Oh, sorry, I get it now, I support @R.MartinhoFernandes on this, a separate type should be used to refer to array slices (which refer to an array) and arrays themselves (which own the data). – avakar Oct 17 '12 at 09:23
  • @marton78 if it keeps a `shared_ptr`, a subarray can live on after the original array leaves (basically, once you're dealing with shared_ptrs, there no such thing as "original" anymore). – R. Martinho Fernandes Oct 17 '12 at 09:26
  • @R.MartinhoFernandes: Sure. That's why I wrote that in this case, the whole discussion applies to subarrays: `y(10,20) = x(10,20);` copies, `subarray y = x(10,20);` creates a new reference. Still inconsistent. – marton78 Oct 17 '12 at 09:39
  • You should make subarrays have reference semantics (i.e. defaulted copy constructor and assignment) and arrays have value semantics (i.e. write copy constructor and assignment by hand that make copies) – R. Martinho Fernandes Oct 17 '12 at 09:41
  • But what should `subarray c = x(10,20); c = y(30,40);` do? 1) Re-reference to `y` or 2) overwrite a part of `x`? When 1), you have to keep in mind if `c` is an array or a subarray. When 2), copy-initialization and copy-assignment behave differently. That's the inconsistency that I mean. – marton78 Oct 17 '12 at 10:00
  • May be not relevant but have you checked std::valarray class or std::slice function? – zahir Nov 30 '12 at 19:30

1 Answers1

1

Does it make more sense for assignment to always create a reference, and copying to be declared explicitly?

In practice, no. Expecting developers to remember that a = b will not copy is what got std::auto_ptr into hot water. The C++11 standards committee also decided on std::move and std::forward to allow developers to explicitly say "I'm not making copies."

What about: array z = x(80,90); In this case, the copy will be elided by the compiler, so no matter what my copy assignment operator does, z will hold a reference to x's storage. There's no way around this.

This was your giveaway. You want "array" and "view to an array" to act differently, but you're representing them both with class array.

Implement a new class arrayview with a similar public interface and move the ranges a_ and b_ to arrayview.

array x, y;
x = y;                  // "array = array" copies all elements
x(30,40) = y(50,60);    // "arrayview = arrayview" copies element range
arrayview v = x(80,90); // "arrayview(const arrayview&) shallow copies
x(30,40) = y;           // NOT DEFINED, since this has no clear meaning
array z = x(80,90);     // NOT DEFINED, since this has no clear meaning

Note that in your arrayview assignment operator, it should be valid to copy from an arrayview with the same resource, but in that specific case you should check whether std::copy_backward is needed instead of std::copy.

Drew Dormann
  • 59,987
  • 13
  • 123
  • 180