Previous section   Next section

Imperfect C++ Practical Solutions for Real-Life Programming
By Matthew Wilson
Table of Contents
Chapter 27.  Subscript Operators


27.1. Pointer Conversion versus Subscript Operators

I mentioned earlier (see section 14.2.2) that one must choose between providing implicit conversion to a pointer (to the managed sequence) or subscript operators. In this section, we'll see why. Consider the code in Listing 27.1.

Listing 27.1.


typedef size_t  subscript_arg_t;


typedef int     indexer_t;


struct DoubleContainer


{


  typedef size_t  size_type;


               operator double const *() const;


               operator double *();


  double const &operator [](size_type index) const;


  double       &operator [](size_type index);


  size_type    size() const;


};


. . .


DoubleContainer dc;


indexer_t       index = 1;


dc[index];  // S#1


dc[0];      // S#2



Most types that provide subscripting can only support non-negative indexes, so most such types define the subscript operator in terms of an unsigned type. The C++ standard (C++-98: 23.1) stipulates that the size_type of containers, including those providing subscript operators, is unsigned, and it usually resolves to size_t.

Many programmers, whether right or wrong, tend to write their indexing code in terms of int. Even if you use size_t for your indexer, unadorned literal integers will be interpreted as a signed integer, usually int (see section 13.2).

The problem we have with a class like DoubleContainer is that subscript syntax can also be applied to pointers. Depending on the types of the subscript operator argument and the indexer variable, our compilers (see Appendix A) exhibit slightly different, but very important differences, as shown in Table 27.1.

Table 27.1.

Compiler

Operator []() argument type

Indexer type

Ambiguous?

Visual C++ 6.0,

int

int

No

Visual C++ 7.1

int

size_t

Yes

 

size_t

int

Yes

 

size_t

size_t

No

Borland,

int

int

No

CodeWarrior,

int

size_t

No

Digital Mars,

size_t

int

No

Watcom

size_t

size_t

No

Comeau, GCC,

int

int

No

Intel

int

size_t

No

 

size_t

int

Yes

 

size_t

size_t

No


The most unpopular combination for our compilers—size_t for the subscript operator argument and int for the indexer—is the one we will most commonly encounter. Clearly, providing both implicit conversion and subscripting together is a nonportable proposition, and I strongly urge you not to bother trying it. Imagine the hassles you'll have if you're working with one of the four that work unambiguously with both and you gaily go ahead and define them. When you move to another compiler, you could have myriad small changes, each one of which will require you to think about the changes involved.

You may think you can be immune by setting the warning level of your compiler to high, to detect use of int for the indexer, and sticking to size_t. Alas, being a good citizen you'll be properly retrieving a pointer to a given array's elements in generic code via the form &ar[0] (see Chapter 33), in which the 0 will be interpreted as an int.[1] To make them work with types such as DoubleContainer, you'd need to rewrite such expressions as &ar[static_cast<size_t>(0)]. Naturally, this will then trip us up on some compilers if the code was applied to types whose subscript argument type was int. Aargh!

[1] Please note that I recognize the partial circularity of this argument. We use &ar[0] notation because we often do not wish to provide implicit conversion. One of the reasons for this is because of the ambiguities when combined with subscripting.

It is a habit of mine to define an index_type member type when writing array classes (see Chapter 33), which could be used in these cases—&ar[static_cast<C::index_type>(0)]—but it's nonstandard, so not of much use in code that needs to be widely applied. In any case, such casts are ugly beyond bearing.

Keep in mind that the use of implicit conversion operators is not a terribly good thing in general, and I'm not advocating otherwise here. But there are circumstances where they are appropriate, and also where they'd be appropriate in concert with subscript operators. Better minds could probably explain why the subscript operator cannot take precedence over the inbuilt subscripting of an implicit conversion operator, but it's largely irrelevant. We're in the real world, and the only sensible solution is to refrain from ever trying to do it.

Imperfection: Provision of implicit conversion operator(s) to pointer type(s) along with subscript operator(s) is nonportable.


Note that the problem even occurs if the base class has the implicit conversion operator, and a derived class provides the subscript operator. This is the reason that pod_vector (see section 32.2.8) uses auto_buffer via composition, rather than via non-public inheritance, which was how I originally had it until trying to use the subscript operators. Note that the compilers still report an ambiguous conversion even when the inheritance is non-public, and the base class's implicit conversion operators are inaccessible to client code of the derived class.

27.1.1 Choose Implicit Conversion Operators

Given that we must choose, what are the circumstances in which we would choose implicit conversion over subscript operators? The only situation I know of is when one must accommodate the widely practiced array to pointer decay. This is why auto_buffer (see section 32.2) provides implicit conversion, since its intent is to be maximally compatible with built-in arrays.

27.1.2 Choose Subscript Operators

In almost all circumstances, I believe one should prefer subscript operators to implicit conversion operators. The reason is that the subscript operator receives the index to be applied from which it calculates the appropriate return value. In the case of implicit conversion, the compiler carries out the index offsetting itself. The advantage of having the index is that it can be validated.



double &DoubleContainer::operator [](size_type index)


{


  . . . // Validate the index


  return m_buffer[index];


}



How we choose to implement this validation is the subject of the next section.


      Previous section   Next section