Smaller c++ questions

Pages: 12
I have read through the whole thread and found some questions that I think never got properly answered.

protoseepp wrote:
f( &i ); // calls (d) with T = int

I was upset at this, because it is a reference to an int and I was sure it would call (e)... f( i );
But no, it calls the template function for a pointer because it provides an address to the pointer. Very sneaky and you have to be extremely careful. Hard for me to catch as a newb.

The & operator is the address-of operator. It gives you the address (i.e. pointer) to the thing on the right.

It's sometimes called the "reference operator" but we're not talking about C++ references here. A pointer can also be considered a kind of reference because it's used to refer to an object. I guess this name makes more sense in C because there are no "real" reference types like in C++.

So if i is an int then the expression &i will give you an int* so it's not surprising that it calls (d). There is no implicit conversion from int* to int so (e) is out of the question.


protoseepp wrote:
If I did the behind the scenes programming for find(), I would have just overloaded it for chars & strings to make it contiguous. They both have 3x overloads each, so 6x total needed, but I am sure they had a good reason to separate them.

The algorithm functions are usually not overloaded for specific types like that (at least not inside the algorithm header). They are just reusable function templates that can be applied to anything that fulfils the requirements.

As far as I can see std::find only have two overloads. One with and one without execution policy. The execution policy can be used to specify if you for example want the algorithm to run in parallel (using threads).

std::search allow you to specify execution policy just like std::find, but it also allow you to provide a "binary predicate" (in case you don't want each element to be compared using ==). All combinations with and without these makes 4 overloads. There is also a fifth overload to specify more advanced search algorithms which might be more efficient for certain kind of data.

None of these overloads are there to handle searching with a specific type.


seeplus wrote:
With std::string (and NOT other std containers), you can dereference .end() iterator for non-empty to get 0

Dereferencing the end() iterator is technically undefined behaviour and I wouldn't be surprised if a standard library implementation checked for this in a debug mode.


protoseepp wrote:
The 2nd parameter "myVec.cbegin() + 5" knows nothing that it is a .cbegin() once it gets to the other side of the method and cannot distinguish itself from a "myVec.cend()".

This is very true. It's just iterators referring to specific positions inside the range.

There is no way to distinguish between myVec.cbegin() + myVec.size() and myVec.cend() because they refer to the same (one-past-the-last-element) position in the range.


protoseepp wrote:
WHY did they decide to treat "myVec.cbegin() + 5);" as if it were a .cend()?????? I understand that you specify beginning of string to end of string in the arguments, but IT IS NOT A .cend()....so why did they choose this way that can throw one off and one can make a mistake.

A single iterator is often used to specify a position in a range.

1
2
3
myVec.insert(myVec.end(), 77); // insert a new element with the value 77 
                               //  at the "end" (one-past-the-last-element)
                               //  position in the vector 

If the function does something with the element that the iterator refers to we're often not allowed to pass an end() iterator because it doesn't refer to a valid element.

1
2
myVec.erase(myVec.begin() + 2); // erase the third element
myVec.erase(myVec.end()); // Not allowed! (UB) 


Two iterators are often used to specify a range. The first iterator specifies the beginning of the range. The second iterator specifies the position that follows the last position in the range.

1
2
3
4
5
6
7
std::vector myVec { 10, 20, 30, 40, 50, 60 };
                    ▲                   ▲
                    ╰────────╮          ╰───╮
std::vector<int> partialCopy(myVec.begin(), myVec.begin() + 5);

  Copy the values from myVec.begin() up to but not including myVec.begin() + 5.

This is totally consistent with passing myVec.begin() and myVec.end() to specify the range of the whole container.

If they instead had included the second iterator in the range (and let end() refer to the last element) then how would you be able to specify an empty range? It might seem pointless to pass an empty range but it's quite often convenient to be able to treat them as any other range. You might for example have a vector that could be empty that you want to sort. If begin() and end() gives you an empty range when the container is empty then you don't need to check before passing them to std::sort.

1
2
//if (!myVec.empty()) // <--- not necessary
    std::sort(myVec.begin(), myVec.end());
Last edited on
Much obliged Peter.


The & operator is the address-of operator. It gives you the address (i.e. pointer) to the thing on the right.
It's sometimes called the "reference operator" but we're not talking about C++ references here. A pointer can also be considered a kind of reference because it's used to refer to an object. I guess this name makes more sense in C because there are no "real" reference types like in C++.
So if i is an int then the expression &i will give you an int* so it's not surprising that it calls (d). There is no implicit conversion from int* to int so (e) is out of the question.



I just tested myself again before looking at your answers and I got it right. Looking at it now I just have to laugh, but I remember at that time all I had stuck in my head (after a long days worth of head inflation)...was reference of and not address to the pointer. I don't think I will be making that mistake again...clear evidence of a newb.
___________


The algorithm functions are usually not overloaded for specific types like that (at least not inside the algorithm header). They are just reusable function templates that can be applied to anything that fulfils the requirements.

As far as I can see std::find only have two overloads. One with and one without execution policy. The execution policy can be used to specify if you for example want the algorithm to run in parallel (using threads).

std::search allow you to specify execution policy just like std::find, but it also allow you to provide a "binary predicate" (in case you don't want each element to be compared using ==). All combinations with and without these makes 4 overloads. There is also a fifth overload to specify more advanced search algorithms which might be more efficient for certain kind of data.

None of these overloads are there to handle searching with a specific type.


I can accept that their choice was governed by the requirement for greater reusability, although I might not have the full appreciation for it just yet. It is done for a wider audience and the masses would get more mileage out of these constrained choices. 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.
The number of overloads was honestly irrelevant, other than to say they were low enough that I could combine them. But now that you mention it...I was on this site and it has 3 of them, but I see now that one is a duplicate constexpr for compiler choice.
https://en.cppreference.com/w/cpp/algorithm/find

template< class InputIt, class T >
InputIt find( InputIt first, InputIt last, const T& value ); (until C++20)

template< class InputIt, class T >
constexpr InputIt find( InputIt first, InputIt last, const T& value ); (since C++20)

template< class ExecutionPolicy, class ForwardIt, class T >
ForwardIt find( ExecutionPolicy&& policy, ForwardIt first, ForwardIt last,
const T& value ); (2) (since C++17)


I believe I hoovered my mouse for the str1.find() and I saw 3 at the time. Now that I go back, depending on whether the argument list is empty or what content is listed I will get a different overloading number. Whatever I had in the argument list last time, showed me 3. So I guess it whittles it down depending on context. Or maybe sometimes it gets hung on an adjacent word, hard to tell now.

charPos = str1.find(); //Shows 5 overloading possibilities
charPos = str1.find("day", charPos + 1); //Shows 4
___________


Dereferencing the end() iterator is technically undefined behavior and I wouldn't be surprised if a standard library implementation checked for this in a debug mode.

For the heck of it, I tried cout'ing it and it compiled...could be undefined & I don't know how to test. I tried going 1x past the .cend() and crash.
___________

At the start of this question I thought there might be some compile time checking for .cend() & .cbegin() and some omniscience (like a Lambda), but after my last post I realize now that this is not the case. The first & second arguments are just addresses sent. The advantages of having an .end/.cend() comes with pitfalls.

Lets just say we wanted to focus on ease of use and did not want to offset the 2nd parameter.
Can we do a compile time check on the 2nd parameter (end address vs any other address), no we cannot because we don't have the address until run time.

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;

So that now we still pay homage to .cend(), can copy an empty vector, consistent with array index copy syntax for range copies, and ease of use (no offset for 2nd parameter needed) and probably millions of dollars worth of possible bugs, and the need for such a post? I am sure there might be a more elegant way to insert the check depending on what the code is doing.

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.

The focal point is not on this smaller sample, but on programs with thousands and millions of lines. Office party with some drinking and then having to go back to crunch or a massive headache one day and things can get a little blurry when your staring at code for hours.

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

protoseepp wrote:
I was on this site and it has 3 of them, but I see now that one is a duplicate constexpr for compiler choice.
https://en.cppreference.com/w/cpp/algorithm/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.
Last edited on
Dereferencing the end() iterator of a std::string is caught at run-time by the Microsoft implementation (debug build):

Debug Assertion failed!
Cannot dereference string iterator because it is out of range (eg. an end iterator)


(This - *name.end() - is not caught by the Microsoft or LLVM static analysers.)
Oh my, yes the calling function knows its own m_cend, but not the one coming in as the argument...unless .cend() is sent specifically...but even then the calling function is still not aware that it is a .cend() as it is just another address where it needs to stop at for the right-side bounds, correct? Somewhere in the calling function there is an iterator, probably a for loop "for (auto it = param1; it != param2; ++it) that also omits the extra offset address.

So that would mean we would have to send in an additional argument and use up more resources, which could also potentially lead to more errors. Problems all around and I have started to accept the necessity of the offset, especially if everyone else has and that it is not such a big error prone problem once you get used to it.

I understand with my scenario when you say you cannot pass an empty container, because you don't have the right-hand extra offset, for which I was incorrectly trying to give you, but not sure what you mean by:


(And because it doesn't allow me to pass an empty range of a non-empty container)


You could do it if the empty range started from anywhere from beginning up until right before the last element, but it is the last element that is your problem.

For the sake of extrapolation lets say there was a 3rd parameter that held .end(), then you could? Yes, you would just use the left & right bounds as if that range was not empty...same way, as a non-empty range, correct???


What does (until C++20) mean? That overload will work until C++20 and it will be removed in C++23 and then only the constexpr version will be used?

@JLBorges, I see thanks (VS latest, Debug x86)
1
2
3
4
5
6
7
8
9
10
11
	//No compile error or run-time error...undefined.
	string tempstr3 = "Hello";
	cout << ">>>>>" << tempstr3[5] << "<<<<<" << endl;

	//No compile error or run-time error...undefined.
	string* p = &tempstr3;
	cout << ">>>>>" << (*p)[5] << "<<<<<" << endl;

	//Debug Assertion failed!
	auto strIter = tempstr3.cend();
	cout << ">>>>>" << *strIter << "<<<<<" << endl;
Last edited on
> No compile error or run-time error...undefined

It is well defined since C++11; the position pos (5) == size()
It would be undefined behaviour if we had written tempstr3[5] = 'a';
This is fine: tempstr3[5] = 0;

operator[]( size_type pos )
If pos > size(), the behaviour is undefined.
...
If pos == size(), a reference to the character with value CharT() (the null character) is returned.

For the first (non-const) version, the behaviour is undefined if this character is modified to any value other than CharT() .
https://en.cppreference.com/w/cpp/string/basic_string/operator_at
> What does (until C++20) mean? That overload will work until C++20
> and it will be removed in C++23 and then only the constexpr version will be used?

C++23 would supersede C++20; but it (the constexpr version) is backward compatible; it will not break existing code.

Oh ok, you can reference \0, or rewrite 0. Once you change it to anything but 0, then it is undefined. If you go past .size(), then undefined too.

Understood, thanks.
protoseepp wrote:
not sure what you mean by:
Peter87 wrote:
(And because it doesn't allow me to pass an empty range of a non-empty container)

If what you proposed was possible then it would work for empty containers. I think we already agree on that.

But for non-empty containers, if I passed the same iterator as both arguments
 
std::vector<int> partialCopy(myVec.begin(), myVec.begin());
then the second argument would get incremented and it would consider it a range consisting of one element.

That's not entirely true. I could pass the end() iterator as both arguments and that would still be considered an empty range
 
std::vector<int> partialCopy(myVec.end(), myVec.end());
but this composes very badly. There are many reasons why I might end up with empty ranges not referring to the end of a container. Normally when I pass an empty range it's not because I know it's an empty range but because it just happened to be empty based on the outcome of something else.

Example
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
#include <iostream>
#include <algorithm>
#include <vector>

// Divides a list of values into groups separated by a separator value.
// The separators are never included in the groups.
// If there are no values between two separators it will result in an empty group.
std::vector<std::vector<int>> split(const std::vector<int>& vec, int separator)
{
	std::vector<std::vector<int>> groups;

	auto groupBegin = vec.begin();

	while (true)
	{
		auto groupEnd = std::find(groupBegin, vec.end(), separator);
		
		groups.emplace_back(groupBegin, groupEnd); // calls the constructor std::vector<int>(groupBegin, groupEnd)
		
		if (groupEnd == vec.end())
		{
			break;
		}
		
		groupBegin = groupEnd + 1;
	} 
	
	return groups;
}

int main() 
{
	// Just some values that we're going to split.
	std::vector<int> vec = {1, 2, 0, 3, 5, 0, 0, 2};
	
	// Split the values into groups separated by the value 0 (zero).
	std::vector<std::vector<int>> groups = split(vec, 0); 
	
	// Print the groups.
	for (const std::vector<int>& group : groups)
	{
		std::cout << ">";
		for (int value : group)
		{
			std::cout << " " << value;
		}
		std::cout << "\n";
	}
}


Output
> 1 2
> 3 5
>
> 2


I hope the example is pretty self-explanatory but I want you to pay attention to line 18. There we sometimes copy an empty range that's not at the end of the container. I didn't need to do anything special to handle that situation. It just works.

I could have made split a bit more generic by making use of templates and iterators so that you could split not only ints stored in vectors but it wasn't necessary for what I wanted to demonstrate here.
Last edited on
Re std::string. It's best to treat std::string as containing .size() chars (which can include null). Also treat .end() as pointing one past the end. Don't think about the terminating null as being part of std::string - unless using the return value from .c-str() when a c-style string is required for a c type function. Although you can access this terminating null, don't! And especially don't alter it from null. As far as C++ is concerned, now that you know about it - forget about it.
A vector of vector<int>'s that separates them into vector<int>'s based on the separator 0. By the time it hits the second zero (...0, 0,...) here, this creates the empty range of a non-empty container. And that is where my bad scenario will encounter the problem...got it!

@Peter, how long have you been coding? Are you an educator or author of a book?

@seeplus, thanks will keep std::string in mind. I think the problem is when you first encounter C++ you are constantly picking up new things and bombarded with new info. The problem is that the info just does not stop or at times seems like it will ever stop. Info is easier to digest in small tidbits, but when you mix it all up you can easily miss the finer details...at least for me now.

> But for non-empty containers, if I passed the same iterator as both arguments
> std::vector<int> partialCopy(myVec.begin(), myVec.begin());
> then the second argument would get incremented and it would consider it a range consisting of one element.

No. As Peter87 has quite clearly explained:

Peter87> Two iterators are often used to specify a range. The first iterator specifies the beginning of the range.
Peter87> The second iterator specifies the position that follows the last position in the range.
https://cplusplus.com/forum/beginner/283569/2/#msg1229872


Try it:
1
2
3
4
5
6
7
8
9
#include <iostream>
#include <vector>

int main()
{
    const std::vector<int> myVec { 0, 1, 2, 3, 4, 5 } ;
    const std::vector<int> partialCopy( myVec.begin(), myVec.begin() ) ;
    std::cout << "partialCopy.empty()? " << std::boolalpha << partialCopy.empty() << '\n' ; // true
}

http://coliru.stacked-crooked.com/a/fbce82253940ee92
if I passed the same iterator as both arguments


Then you have effectively something like:

1
2
for (auto itr {myVec.begin()}; itr != myVec.begin(); ++itr)
    // body - process itr 


So the loop terminates immediately without executing its body.
Thanks for bringing that up, because that is exactly what I thought when I saw begin/begin and end/end....and thought how was that even possible without it exiting the loop right away...but I am too tired to even think about it now. Need sleep and I will look at it with fresh brain cells.

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <iostream>
#include <vector>

int main()
{
    const std::vector<int> myVec{ 0, 1, 2, 3, 4, 5 };
    const std::vector<int> partialCopy(myVec.begin(), myVec.begin());
    std::cout << "partialCopy.empty()? " << std::boolalpha << partialCopy.empty() << '\n'; // true

    
    const std::vector<int> partialCopy2(myVec.end(), myVec.end());
    std::cout << "partialCopy2.empty()? " << std::boolalpha << partialCopy2.empty() << '\n'; // true
}



partialCopy.empty()? true
partialCopy2.empty()? true


Last edited on
how long have you been coding?

It's 17 years since I first started to learn programming in school.

Are you an educator or author of a book?

No.
Less the mix-ups & the begin/begin and end/end, I get the gist of everything else though. Good lesson in futility on my part too and further appreciation for how they went about it behind the scenes.
@JLBorges
You corrected me by quoting me. :)

I was talking about this alternative world that protoseepp proposed where the second argument was incremented unless it was equal to the end iterator.

if (param2 != m_cend)
++param2;
Peter87 wrote:
if I passed the same iterator as both arguments
seeplus wrote:
Then you have effectively something like:
1
2
for (auto itr {myVec.begin()}; itr != myVec.begin(); ++itr)
    // body - process itr  
So the loop terminates immediately without executing its body.

Yes, I know that's how it works.

But protoseepp suggested the second parameter should get incremented internally (unless it's equal to the end() iterator) so that the second argument specified the last element in the range rather than one-past-the-last element. This was the context in which I wrote that post. I'm sorry if it caused confusion.
Last edited on
Just want to say thanks again Peter, you have an exceptional way of pinpointing what the questioner might be missing and giving examples in such a way that clarifies things.

I have conceded to dropping my request to change param2 without the offset, and I understand why now completely...lets just scratch that notion and thanks immensely for helping me see this.
I know that you told me that (begin, begin) & (end, end) produces an empty vector, and I saw that it is true and that you are correct. But just looking at that as a human my first thought was...how is that even possible...and I had to accept it but needed to dissect it to have a better connection.

When you have these two cases the for loop drops immediately, and you really aren't copying anything. What instead must be happening is that there might be a check somewhere:

if (param1 == param2)
//then just make an empty partialCopy vector

And so with this everything seems to fit in nicely when .cend() offset is kept in mind. BUT as a human when you say it in these terms... param1 is begin, but param2 is also begin that represents +1 offset, and you really are then pointing to begin-1...but no, no the code behind the scene does not do that, because the param2 is there nicely to end the for loop. So there is some trickery there when you supplant the human process with the actual code. As a human you are also expecting that when you say include my left finger pointing to begin (10) and right finger pointing to (10), you better not give me an empty container, because I was pointing at 10...even with an offset.

But I understand though. So much for smaller c++ questions, probably best to end this thread & make a smaller questions part 2 with actual smaller questions.
protoseepp wrote:
I know that you told me that (begin, begin) & (end, end) produces an empty vector, and I saw that it is true and that you are correct. But just looking at that as a human my first thought was...how is that even possible...and I had to accept it but needed to dissect it to have a better connection.

Programming has a lot in common with math. It's probably easier to understand this if already familiar with open and closed intervals.

https://en.wikipedia.org/wiki/Interval_(mathematics)#Terminology

Iterator ranges are essentially half-open intervals. The lower bound is closed (i.e. included in the range). The upper bound is open (i.e. excluded from the range). With common math syntax this could be written as [begin, end)

(the words open and closed makes more sense when dealing with intervals of real (decimal) numbers)

protoseepp wrote:
When you have these two cases the for loop drops immediately, and you really aren't copying anything. What instead must be happening is that there might be a check somewhere:

if (param1 == param2)
//then just make an empty partialCopy vector

Maybe, but not necessarily. I can imagine they might check to avoid using new when the size is empty but it's not really necessary because new can allocate zero-sized arrays just fine (this is somewhat complicated by the fact that containers use allocators and don't use new directly). This would be more of an optimization.

protoseepp wrote:
as a human when you say it in these terms... param1 is begin, but param2 is also begin that represents +1 offset, and you really are then pointing to begin-1

That's the wrong way to think about it. Don't think about any offsets. Both iterators refer to the same position. It's just that when you use two iterators to represent a range the first one represent where you start, and the second one represent where you stop and that position is not included in the range.

protoseepp wrote:
As a human you are also expecting that when you say include my left finger pointing to begin (10) and right finger pointing to (10), you better not give me an empty container, because I was pointing at 10

If I told you to count from (10) up to but not including (10) then I would expect you to count no numbers.

But you've got a point that this is probably not intuitive for most people that are not used to it. In a real-life situation I don't think you would have pointed to any of the numbers at all if you told me not to count them.
Last edited on
Registered users can post here. Sign in or register to post.
Pages: 12