6

I'm trying to create a "sparse" vector class in C++, like so:

template<typename V, V Default>
class SparseVector {
    ...
}

Internally, it will be represented by an std::map<int, V> (where V is the type of value stored). If an element is not present in the map, we will pretend that it is equal to the value Default from the template argument.

However, I'm having trouble overloading the subscript operator, []. I must overload the [] operator, because I'm passing objects from this class into a Boost function that expects [] to work correctly.

The const version is simple enough: check whether the index is in the map, return its value if so, or Default otherwise.

However, the non-const version requires me to return a reference, and that's where I run into trouble. If the value is only being read, I do not need (nor want) to add anything to the map; but if it's being written, I possibly need to put a new entry into the map. The problem is that the overloaded [] does not know whether a value is being read or written. It merely returns a reference.

Is there any way to solve this problem? Or perhaps to work around it?

Thomas
  • 174,939
  • 50
  • 355
  • 478
  • 2
    boost::mapped_vector<> should do something similar - you can study it for ideas (or maybe just use it). – Michael Burr Sep 06 '09 at 17:10
  • It doesn't support my Default values, and also, I was going to do this for a two-dimensional matrix, so using it directly is out of the question. But still a useful reference! – Thomas Sep 06 '09 at 19:08

3 Answers3

13

There may be some very simple trick, but otherwise I think operator[] only has to return something which can be assigned from V (and converted to V), not necessarily a V&. So I think you need to return some object with an overloaded operator=(const V&), which creates the entry in your sparse container.

You will have to check what the Boost function does with its template parameter, though - a user-defined conversion to V affects what conversion chains are possible, for example by preventing there being any more user-defined conversions in the same chain.

Steve Jessop
  • 273,490
  • 39
  • 460
  • 699
  • This is the only solution. On the other hand, proxy objects aren't perfect either - for example, if `V` had any overloaded conversion operators, a proxy won't be able to seamlessly propagate them. – Pavel Minaev Sep 06 '09 at 17:22
  • Ah just tested the boost vectors. Looks like they behave this way too. Interesting. – Johannes Schaub - litb Sep 06 '09 at 17:38
  • (And `vector` isn't an STL container either.) – sbi Sep 06 '09 at 22:22
  • For the use of the boost algorithm it is probably okay, however if one would expect to catch the result with a temporary variable (say a V&) what happens ? – Matthieu M. Sep 30 '09 at 16:15
  • Um, without testing it I'd say compilation failure (if the reference is non-const), or a temporary bound to the variable (if the reference is const). `operator[]` is optional in the Sequence interface (23.1.1). This container doesn't implement it as defined in that table (returning an lvalue of T and taking amortized constant time as specified in table 68 and clause 12 respectively): algorithms which take iterators only require that `operator*` be assignable from T, not that it return `T&`, and I'm expecting/hoping that the same is true of this unspecified Boost function which uses [] instead. – Steve Jessop Sep 30 '09 at 16:49
9

Don't let the non-const operator& implementation return a reference, but a proxy object. You can then implement the assignment operator of the proxy object to distinguish read accesses to operator[] from write accesses.

Here's some code sketch to illustrate the idea. This approach is not pretty, but well - this is C++. C++ programmers don't waste time competing in beauty contests (they wouldn't stand a chance either). ;-)

template <typename V, V Default>
ProxyObject SparseVector::operator[]( int i ) {
   // At this point, we don't know whether operator[] was called, so we return
   // a proxy object and defer the decision until later
   return ProxyObject<V, Default>( this, i );
}

template <typename V, V Default>
class ProxyObject {
    ProxyObject( SparseVector<V, Default> *v, int idx );
    ProxyObject<V, Default> &operator=( const V &v ) {
      // If we get here, we know that operator[] was called to perform a write access,
      // so we can insert an item in the vector if needed
    }

    operator V() {
      // If we get here, we know that operator[] was called to perform a read access,
      // so we can simply return the existing object
    }
};
Frerich Raabe
  • 90,689
  • 19
  • 115
  • 207
  • Is there any way to have a type be convertible to e.g. both a `const int &` and an `int &`, and have the compiler select the former whenever it can use a non-writable lvalue? I'd like to have a proxy object return a reference to a field which is loaded form the main object, and then have its destructor copy the field back to the main object if it may have changed, but I can't figure out how to avoid the write-back on a read-only access. – supercat Mar 20 '15 at 18:40
1

I wonder whether this design is sound.

If you want to return a reference, that means that clients of the class can store the result of calling operator[] in a reference, and read from/write to it at any later time. If you do not return a reference, and/or do not insert an element every time a specific index is addressed, how could they do this? (Also, I've got the feeling that the standard requires a proper STL container providing operator[] to have that operator return a reference, but I'm not sure of that.)

You might be able to circumvent that by giving your proxy also an operator V&() (which would create the entry and assign the default value), but I'm not sure this wouldn't just open another loop hole in some case I hadn't thought of yet.

std::map solves this problem by specifying that the non-const version of that operator always inserts an element (and not providing a const version at all).

Of course, you can always say this is not an off-the-shelf STL container, and operator[] does not return plain references users can store. And maybe that's OK. I just wonder.

sbi
  • 219,715
  • 46
  • 258
  • 445
  • About the soundness of the design: it's a memory optimization of a general vector, to be used in specific cases - so the interface/contract/documentation should cover that. About the validity of references: even the stl iterators aren't always valid. – xtofl Sep 06 '09 at 20:11
  • 2
    @xtofl: however, STL containers define very carefully what operations can invalidate iterators and/or references to elements of the container. This container should do the same, and users must ensure that any templates using the class as a parameter don't make demands that it can't satisfy. – Steve Jessop Sep 06 '09 at 20:37