So, the last blog entry focused on optimizing initializer_list insertions by using a proxy function. Oh, and about that – proxy functions as I term them can be thought of as an intermediary or convenience functions (if it helps). Anyway, I realized after I wrote that entry that, in terms of good programming practices, the code wasn’t exactly up to par.
When using iterators to insert items into a container, the preference is to always use insert iterators. The reason is quite obvious – they are there to insert items into a container! Plus, there’s a guarantee of safety with these iterators. They don’t get invalidated – which is what would happen to that vector proxy function if it failed to take a difference of the distance between the iterator position and the container start. (and again here, std::distance() could have been used). Nonetheless, I accounted for that and also made sure to reassign the iterator after every insert. The net result was that it worked, but it might not have been clear as to what it did. So.. lets consider the 3 types of insert iterators, which are very clear about what they do..
- Back inserter (back_inserter_iterator): This iterator does just one simple thing – it calls a container’s push_back() function every time an assignment operator is used with it.
- Front inserter (front_insert_iterator): Like back_inserter, this does one thing as well – it calls a containers push_front() function with each assignment.
- General inserter (insert_iterator): This iterator is a little different. It does call only one function – insert() – however, it also increments the returned iterator by 1. This is just like the code in the last proxy function (++pos).
Each of the above iterators use a simple, common interface, just like every other iterator. *it, it++, ++it, and **it = data are what we use to interact with them, though what happens behind the scenes is a little different. For example, given this code:
std::vector<std::string> myVec; std::back_insert_iterator<std::vector<std::string>> myIns(myVec); *myIns++ = "strA"; *myIns++ = "strB";
We might expect that for each *myIns++, the item at the inserter position is dereferenced, assigned to, and then the inserter is moved on to the next position. But with all insert iterators, only one operator here makes any changes – the assignment operator. The pre-increment (++it), post-increment (it++), and even the dereference (*it) operators all do nothing but return a reference to the insert iterator. Which means the below code gives exactly the same results:
std::back_insert_iterator<std::vector<std::string>> myIns(myVec); myIns= "strA"; // calls push_back() myIns= "strB"; // and again
I’ve gone off topic as you may have noticed, but mainly because I find it interesting to plumb into the depths of C++’s inner workings. Insert iterators are clever in that they allow code to be used in template functions that expect the typical ‘*it++ = data‘ to work in a consistent manner. And it does, of course.. just not in the way some might imagine. So that’s just a bit of neat trivia for you.. insert iterators are mainly single-function container calls disguised as iterator objects. Well, with the exception of insert_iterator – which also does an increment. In fact, that code generally looks like this:
insIter& operator=(const T& val) // (note - theres also an RValue version) { it = container->insert(it, val); ++it; return *this; }
As you might notice, iterators store a pointer to a container. That’s how and why they can be used wherever a template function takes an iterator. =)
Oookaay, so since I’ve wasted your precious time with that background on insert iterators, let’s just go ahead and throw the new initializer_list proxy function out there:
// initializer_list Proxy - Using Insert Iterators template <typename InsIter, typename LType> void insertInitListProxy(InsIter inserter, std::initializer_list<LType> il) { // construct & move the items into the container for (auto it = il.begin(); it != il.end(); ++it) *inserter++ = *it; }
Looks simple enough? In theory we can remove the *dereference and ++ increment operators from inserter, but then that would make the code less understandable – and more importantly, less flexible. There’s not telling what other things could be thrown at it, or what new types of insert iterators could be used. And that’s one of the great things about template functions – we offer generic support for both known and unknown types of objects that might be thrown its way.
Ah, and another nice thing with insert iterators – there’s convenience functions available to make passing them to other functions that much easier. These functions are back_inserter, inserter, and front_inserter – and what they do is obvious, they create xyz_insert_iterator objects. So the call to insert objects using my proxy function with a back inserter would now look like this:
insertInitListProxy(std::back_inserter(myVec), {"str1", "str2", "str3"});
Simple enough? The only other thing I’d like to note is that, although push_back, insert, and push_front are used, the C++11 standard gives us RValue versions of these. So what happens here is the strings are constructed and then moved into the container. Waste not, want not!
Okay, as is the norm, I have code up @ Coliru demonstrating the above, using that StringHolder object from last time. And the code @ PasteBin.
Ah, and as always – one last thing to add. ostream_iterator and ostreabuf_iterator also ignore the * and ++ operations, and only call a function when an assignment operator is used. (istream_iterator and istreambuf_iterator work a little differently, however)