protoseepp wrote: |
---|
I don't know Python, but I get the feeling that this wouldn't be a question with that language & that these functions/algorithms would be more intuitive. |
You already have more "intuitive" functions as members of std::string.
https://en.cppreference.com/w/cpp/string/basic_string/find
Look at the right. You see the first two has the same number. The first one without constexpr says (until C++20) and the second one with constexpr says (since C++20). So they aren't really two different overloads. It's just that C++20 added constexpr.
It's pages like that that make me wish cppreference.com could show overloads for a single version of the C++ standard at a time and hide the rest, and it wouldn't hurt separating the different function names or making them stand out better, because right now it can be hard to get a good overview with all the noise.
protoseepp wrote: |
---|
For the heck of it, I tried cout'ing it and it compiled...could be undefined & I don't know how to test. |
That's the problem with undefined behaviour. You can't test for it. You can see if something goes wrong, and certain compiler flags and debuggers might make it easier to detect, but if things just seems to "work" that doesn't mean it's guaranteed to work.
Dereferencing the end() iterator of a std::vector generates a runtime error with GCC when compiling with
-D_GLIBCXX_DEBUG (to enable the debug version of the library). Doing the same with std::string does not generate an error for some reason. Maybe it's an oversight, maybe they have always allowed it and complaining about it would do more harm than good to code that already uses it and relies on it to work, or maybe I am wrong (but in that case cppreference.com is also wrong).
protoseepp wrote: |
---|
The advantages of having an .end/.cend() comes with pitfalls. |
Yes, but so does not having it too.
protoseepp wrote: |
---|
So then lets take it to the overloaded copy constructors. partialCopy knows internally where the end address is. So why not this...
if (param2 != m_cend)
++param2; |
Because that would be inconsistent with how it's done elsewhere.
(And because it doesn't allow me to pass an empty range of a non-empty container)
EDIT: I just realized this doesn't work at all because the m_cend that we're interested in doesn't belong to the vector that is being constructed (partialCopy). Instead it would belong to the thing that is copied from (i.e. myVec), but that doesn't even have to be a vector.
Note that we normally don't call this a "copy constructor". A copy constructor is the constructor that takes a single argument of the same type as we are constructing (e.g. passing a std::vector<int> to the constructor of std::vector<int>).
protoseepp wrote: |
---|
Peter, have you seen my prior post on this thread? |
How often do you find bugs/errors from others, or have caught yourself getting the wrong range for containers? Eh, why not same question for the zero-based arrays while we are on this relevant topic. |
I think the important thing is consistency. Because we use zero-based indexing everywhere it's not a problem. Same with iterators. Every function that takes a range using iterators work the same so programmers know what to expect. That's why I think people make relatively few mistakes.
If I try to consider all the advantages and all the disadvantages I personally think this is a pretty good approach.
Consider just a plain loop using iterators:
1 2 3 4
|
for (auto it = begin; it != end; ++it)
{
doSomething(*it);
}
|
This works every time, whether or not the range is empty.
If we instead use an iterator
last that refers to the last element in the range then we no longer have a way to represent empty ranges (unless maybe some kind of before-the-first-element iterator was used when the range was empty, but this comes with other problems). Another problem is how you write the loop?
Do you use
<= instead of
!= ?
|
for (auto it = begin; it <= last; ++it)
|
What about non-random access iterators (like std::list<int>::iterator) that doesn't support the <= operator?
If you are still allowed to advance iterators one-past-the-last-element you could write the loop as before.
1 2
|
auto end = std::next(last);
for (auto it = begin; it != end; ++it)
|
But that means
all code that use iterators would have to do this extra bit of work to get the end iterator, and that's more error-prone because that's one more thing that you might get wrong.
I'm also a bit unsure how the above approach would work with "input iterators" (
https://en.cppreference.com/w/cpp/named_req/InputIterator ) which only requires you to be able to pass through the sequence once (the end iterator is therefore kind of special).
This is an example of using a std::istream_iterator<int> to read a list of integers from file.
1 2 3 4 5 6 7 8 9 10 11 12 13 14
|
#include <iostream>
#include <iterator>
#include <fstream>
int main()
{
std::fstream numfile{"numbers.txt"};
auto begin = std::istream_iterator<int>{numfile};
auto end = std::istream_iterator<int>{};
for (auto it = begin; it != end; ++it)
{
std::cout << *it << "\n";
}
}
|
You're only allowed to step through the sequence once using std::istream_iterator. In a file you might think you can jump around and read the content as many times as you like but don't forget that there are many other sources that a stream could be reading from. You could use the same approach to read numbers from the user (std::cin) or from some non-standard stream that reads over the internet or whatever. In some of these cases you might not know what the last element is, or you might not know that it was the last one until you have read it, so that makes it impossible to provide an iterator to that element up-front.
I'm not saying there aren't other approaches that could have worked, but it at least shows there are many things to consider when coming up with a robust and consistent approach that can be used everywhere.