![]() | |
![]() ![]() |
![]() | Imperfect C++ Practical Solutions for Real-Life Programming By Matthew Wilson |
Table of Contents | |
Chapter 27. Subscript Operators |
27.1. Pointer Conversion versus Subscript OperatorsI 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.
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!
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.
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 OperatorsGiven 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 OperatorsIn 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. ![]() |
![]() | |
![]() ![]() |