- 23.1 Introduction
- 23.2 The Fibonacci Sequence
- 23.3 Fibonacci as an STL Sequence
- 23.4 Discoverability Failure
- 23.5 Defining Finite Bounds
- 23.6 Summary
23.5 Defining Finite Bounds
There are two clear and related solutions to this problem.
- Have end() return an iterator in the range [begin(), ∞) whose value does not overflow the value type.
- Allow the user to specify an upper limit for the effective range provided by the sequence, represented in the value returned by end(). This value would have to be within the valid range.
A good implementation would provide both, where solution 1 is merely the default form of solution 2. We'll examine this by looking at the user-specified limits first.
23.5.1 Iterators Rule After All?
Before we proceed, I must cover an issue that some readers may now be considering. Earlier I ruled out the representation of Fibonacci sequences as independent iterators. The cunning linguists among you may be considering a form that does exactly that, as in:
std::copy(Fibonacci_iterator(), Fibonacci_iterator() + 40 , std::ostream_iterator<Fibonacci_sequence::value_type>(std::cout , " "));
In this case, the putative Fibonacci_iterator would implement the addition operator, such that the expression Fibonacci_iterator() + 40 would evaluate to an instance that would terminate the iteration of a default-constructed iterator on its fortieth increment. At first blush this seems like an adequate solution to the problem.
However, the problem is that use of the addition operator on an iterator indicates that the iterator type is a random access iterator. Further, random access iterators have constant time complexity. To be sure, we're perforce violating pure STL requirements here and there in STL extension. But such violations are never done without due care and particular attention to the effects on discoverability and the Principle of Least Surprise. For example, it's hard to imagine that users of the InetSTL findfile_sequence class (Section 21.1), an STL collection that provides iteration of remote FTP host directory contents, will assume any particular complexity guarantees, given the vagaries of Internet retrieval. However, I suggest that it's far more likely that someone would assume constant time seeing pointer arithmetic syntax on an iterator.
Further, since a user will reasonably expect to be able to type *(Fibonacci_iterator() + 40) if he or she can type Fibonacci_iterator() + 40, we'd have to implement full random access semantics. But, as far as I know, there's no constant-time function integral formula with which you can determine the Nth value of the Fibonacci sequence. (There are a couple of formulas that may be used, but they rely on the square root of five, which would rely on floating-point calculation. One of them is ((1 + sqrt(5)) / 2) - ((1 - sqrt(5)) / 2) ^ n. I'm just enough of a computer numerist to know that I know far too little about floating point to be confident of writing a 100% correct sequence using floating-point calculations.)
Thus we would have to perform a number of forward or backward calculations to arrive at the required value, which is a linear-time operation. This would be a very unobvious violation of a user's expectations and is, in my opinion, unacceptable.
(Of course, we could provide amortized constant time by storing the calculated values in an array. We could go further and provide a static member array with precalculated values. We could even use template metaprogramming and effect compile-time calculation. But the purpose of this chapter is pedagogical. Feel free to do any of these, and let me know how it goes. I'll gladly post interesting solutions on the book's Web site.)
23.5.2 Constructor-Bound Range
One use case of a sequence might be to retrieve the first N elements in the sequence. It would be straightforward to implement the sequence and iterator classes such that you would specify the number of elements in the sequence constructor, which would then return a bounding iterator instance via its end() method, as shown in Listing 23.7. (This corresponds to Fibonacci_sequence_5.hpp on the CD.)
Listing 23.7. Version 5: Constructor and end() Method
public: // Construction explicit Fibonacci_sequence(size_t n) // Max # entries to enumerate : m_numEntries(n) {} . . . public: // Iteration const_iterator begin(); const_iterator end() { return const_iterator(m_numEntries); // Define end of sequence } . . .
You could use this as follows:
Fibonacci_sequence fs(25); std::copy(fs.begin(), fs.end() , std::ostream_iterator<Fibonacci_sequence::value_type>(std::cout));
This could be implemented by adding an additional m_stepIndex member to const_iterator, which would be incremented each time operator ++() is called, and by evaluating equality (in equal()) by comparing the m_stepIndex members of the comparands (Listing 23.8).
Listing 23.8. Version 5: const_iterator
class Fibonacci_sequence::const_iterator : . . . // As shown previously { public: // Construction const_iterator(value_type i0, value_type i1) : m_i0(i0) , m_i1(i1) , m_stepIndex(0) {} const_iterator(size_t stepIndex) : m_i0(0) , m_i1(0) , m_stepIndex(stepIndex) {} public: // Iteration class_type& operator ++() { . . . // Perform the advancement summations as before ++m_stepIndex; return *this; } public: // Comparison bool equal(class_type const& rhs) const { return m_stepIndex == rhs.m_stepIndex; } . . .
This is a nice solution, and it also allows us to meaningfully support the empty() method. However, there's an equally valid use case, that of constraining the enumeration within a given integral range, for example, to enumerate all entries less than the value 1,000,000,000. The sequence might look like that shown in Listing 23.9. (This implementation corresponds to Fibonacci_sequence_6.hpp on the CD.)
Listing 23.9. Version 6: Constructor and end() Method
class Fibonacci_sequence { . . . public: // Construction explicit Fibonacci_sequence(value_type limit); // Value ceiling : m_limit(limit) {} . . . public: // Iteration const_iterator begin(); const_iterator end() { return const_iterator(m_limit); // Define sequence ceiling } . . .
Comparison would be conducted by the somewhat abstruse implementation of equal() shown in Listing 23.10. (There's an overflow bug in here, which I've left since this is a pedagogical class. Try setting the limit to 1,836,311,904 for a 32-bit unsigned value type. If readers want to implement the full testing for overflow, I'll be happy to post any correct solutions on the book's Web site.)
Listing 23.10. Version 6: const_iterator::equal() Method
bool Fibonacci_sequence::const_iterator::equal(class_type const& rhs) const { if( 0 != m_i1 && 0 != rhs.m_i1) { // Both definitely normal iterable instances return m_i0 == rhs.m_i0 && m_i1 == rhs.m_i1; } else if(0 != m_threshold && 0 != rhs.m_threshold) { // Both definitely threshold sentinel instances return m_threshold == rhs.m_threshold; } else { // Heterogeneous mix of the two types if(0 == m_threshold) { return m_i0 >= rhs.m_threshold; } else { return rhs.m_i0 >= m_threshold; } } }
A more flexible class would accommodate both these usage models. But doing so presents the sticky problem of how to unambiguously construct an instance of the sequence for either use case. One solution would be to use a two-parameter constructor, as follows:
. . . public: // Construction Fibonacci_sequence(size_t n, value_type limit); . . .
The parameter for the end-marker type not used would be given a stock value, for example:
Fibonacci_sequence(0, 10000); // This uses a limit of 10,000 Fibonacci_sequence(20, 0); // This gives a sequence of 20 entries
Obviously, this is an inelegant and highly error-prone approach. A slightly less revolting alternative would be to use an enumeration to indicate the type of end marker required and use a value_type parameter for both the threshold and the number of entries:
. . . public: // Member Constants enum LimitType { thresholdLimit, countLimit }; public: // Construction Fibonacci_sequence(value_type limit, LimitType type); . . .
23.5.3 True Typedefs
The best solution uses true typedefs (Section 12.3), which facilitate the unambiguous overloading of essentially similar or even identical types. The final implementation of the Fibonacci_sequence does this, as shown in Listing 23.11. (This corresponds to Fibonacci_sequence_7.hpp on the CD.) Note the use of precondition enforcements in both constructors. A valid design alternative would be to throw std::out_of_range (since the user's value is not predictable).
Listing 23.11. Version 7: Class Declaration and Traits Class
template <typename T> struct Fibonacci_traits; template <> struct Fibonacci_traits<uint32_t> { static const uint32_t maxThreshold = 2971215073; static const size_t maxLimit = 47; }; template <> struct Fibonacci_traits<uint64_t> { static const uint64_t maxThreshold = 12200160415121876738; static const size_t maxLimit = 93; }; class Fibonacci_sequence { public: // Member Types typedef ?? uint32_t or uint64_t ?? value_type; typedef Fibonacci_traits<value_type> traits_type; typedef true_typedef<size_t, unsigned> limit; typedef true_typedef<value_type, signed> threshold; class const_iterator; public: // Construction explicit Fibonacci_sequence(limit l = limit(traits_type::maxLimit)) : m_limit(l.base_type_value()) , m_threshold(0) { STLSOFT_MESSAGE_ASSERT( "Sequence limit exceeded" , l <= traits_type::maxLimit()); } explicit Fibonacci_sequence(threshold t) : m_limit(0) , m_threshold(t.base_type_value()) { STLSOFT_MESSAGE_ASSERT( "Sequence threshold exceeded" , t <= traits_type::maxThreshold()); } public: // Iteration const_iterator begin() const; const_iterator end() const { return (0 == m_limit) ? const_iterator(m_threshold) : const_iterator(m_limit, 0); } public: // Size bool empty() const { return 0 == m_limit && 0 == m_threshold; } size_t max_size() const { return traits_type::maxLimit; } private: // Member Variables const size_t m_limit; const value_type m_threshold; };
Note the use of the traits. Although they're not required by the definition of the sequence as it stands, they serve two important purposes. First, they provide a clear and obvious place for the limit and threshold magic numbers to reside, as well as making them largely self-documenting. Second, should you choose to use a 32- or 64-bit value type, the change involves just a single line.
The iterator class can now be defined as shown in Listing 23.12.
Listing 23.12. Version 7: const_iterator
class Fibonacci_sequence::const_iterator : . . . // As shown previously { public: // Member Types typedef Fibonacci_sequence::value_type value_type; typedef Fibonacci_sequence::const_iterator class_type; private: // Construction friend class Fibonacci_sequence; const_iterator(); const_iterator(Fibonacci_sequence::limit lim); const_iterator(Fibonacci_sequence::threshold t); . . . // Iteration and Comparison methods as before };
With this definition, all the following are well defined (and thereby value constrained):
typedef Fibonacci_sequence fibseq_t; fibseq_t fs(fibseq_t::limit(0)); // Empty sequence fibseq_t fs(fibseq_t::limit(1)); // 1 value fibseq_t fs(fibseq_t::limit(10)); // 10 values fibseq_t fs(fibseq_t::limit(47)); // 47 values fibseq_t fs; // 47 values fibseq_t fs(fibseq_t::threshold(0)); // Empty sequence fibseq_t fs(fibseq_t::threshold(1)); // 1 value fibseq_t fs(fibseq_t::threshold(2)); // 3 values fibseq_t fs(fibseq_t::threshold(47)); // 10 values fibseq_t fs(fibseq_t::threshold(100)); // 12 values fibseq_t fs(fibseq_t::threshold(1000000000)); // 45 values
Equally important, the following are not well defined, and the user knows this because he or she can evaluate them against the member constants Fibonacci_sequence::traits_ type::maxLimit and Fibonacci_sequence::traits_type::maxThreshold. Furthermore, because of the enforcements placed in the constructor bodies, the user finds out immediately when something is wrong, rather than at a later point during enumeration when the values overflow.
fibseq_t fs(fibseq_t::limit(50)); // Breaks ctor precond fibseq_t fs(fibseq_t::threshold(2971215075)); // Breaks ctor precond