Pragmatically? Its about clear communication
Fundamentally? Its about coupling.
Express the intentions of the code clearly.
As to what clearly means is a discussion in and of itself and everyone will arrive at their own definition.
Precision capabilities
When I'm in full control of the data-structure (its function local or a private variable of the object/class) any changes I make to the data-structure will not ripple out very far.
As a consequence this also entices me to keep my objects smaller as changing the data-structure in a 200 line class is far simpler than a 200k line class. So I naturally get high cohesion.
Because the scope is kept small, we can afford higher coupling, and use the full capabilities of the data-structure and its exact nature to express the functions with clarity. Such as by using array indexing, and memory layout.
With this clarity I can easily spot bugs, and make modifications.
The first example is a case of the client code is treating the string
as a data-structure:
for (int i = 0; i < str.length(); i++) {
char c = str[i];
}
But it does mean that string
must fill the role of a data-structure. It must always provide those exact properties and that exact interface:
int length() const; //O(1) and length() <= MAX_INT
T operator[](int); //O(1) and return type is convertable to char in O(1)
and the defaults over int
++int;
int < int;
Not to mention everything else a string must provide to be considered stringy... But as it isn't used we can ignore it when substituting for another data structure.
But there are not many data-structures that can offer that kind of performance, or capabilities.
- Contiguous arrays are pretty much it.
- Perhaps a sparse hash table... but only up to a point.
- Perhaps a constant time expression for calculating the ith member of the sequence.
And even those that do are still limited by the fact that int
has a maximum value.
Just the necessary
When I do not have full control, I prefer to treat it as a service provider so that my code is as general as it can be. This does force me to consider exactly what my function needs, which often leads me to a better understanding of the issue.
The main advantage to this approach is that both the user and provider do not overly restrict each other. The provider is not forced to provide anything more than the exact capabilities, and the user does not demand more than is absolutely necessary.
This makes the function much more reusable, and I believe clearer.
for (auto i = str.begin(); i != str.end(); ++i) {
char c = *i;
}
The second example treats the string
as a service provider. It requests a specific kind of access, in this case sequential read access.
The string is free to provide this access as it sees fit within the contract of the interface:
iterator begin(sequence); //O(1) something that has forward sequential access
iterator end(sequence); //O(1) something to signify when the forward access has accessed everything
bool operator!=(iterator, iterator); //O(1) a test to determine when the forward access has reached this point in the sequence
iterator iterator.operator++(); //O(1) something to advance the forward sequential access to the next location
T iterator.operator*(); //O(1) something to access that next element.
This interface fits a specific use case (forward sequential access) like a glove.
- The user does not demand any more functionality
- The provider need not provide further functionality
The difference is that a Tree could provide the forward sequential access that satisfies this second interface where it could never satisfy the O(1) random access of the first interface, even though the service required by the user is forward sequential access.