Appending Values to a Vector with Boost.Assign
C++11 has simplified the syntax to initialize an STL collection with values. Before C++11 we had to write this:
std::vector<int> v; v.push_back(1); v.push_back(2); v.push_back(3); v.push_back(4); v.push_back(5); v.push_back(6); v.push_back(7); v.push_back(8); v.push_back(9); v.push_back(10);
Now C++11’s std::initializer_list
allows to write that instead:
std::vector<int> v = {1,2,3,4,5,6,7,8,9,10};
But what if v
is an existing vector, to which we’d like to append new values? Then we can’t use a std::initializer_list
, and we’re stuck with the cumbersome pre-C++11 syntax.
At least, with the standard library. But Boost offers the Assign library, which allows for a natural syntax for appending values to a vector:
#include <boost/assign/std/vector.hpp> using namespace boost::assign; int main() { std::vector<int> v; v += 1,2,3,4,5,6,7,8,9,10; }
And it also works with a set:
std::set<int> v; v += 1,2,3,4,5,6,7,8,9,10;
Let’s see how this nice interface is implemented.
The surprising precedence of the comma operator
Let’s take the example with the vector:
std::vector<int> v; v += 1,2,3,4,5,6,7,8,9,10;
How do you think this is implemented? Clearly, there is some operator overloading at play, with operator+=
and operator,
(did you know that we could overload the comma operator in C++?).
At first glance, the expression 1,2,3,4,5,6,7,8,9,10
seems to resolve to some sort of list, and operator+=
should tack on the elements of this list to the vector v
.
But that would be a bold thing to do. Indeed, it would overload operator,
for int
s, and more generally for any type, because any type (or pretty much) can be in a vector. On top of being intrusive for the custom type, this goes directly against the guideline of not overloading the comma operator, given in item 8 of More Effective C++.
But the implementation of Boot Assign doesn’t work like that. To understand what it does exactly, we need to better predict what is going on with the comma operator.
To illustrate, consider the following piece of code that uses the comma operator, that Fluent C++ reader jft published as a comment to the article on the comma operator:
int a = 1; int b = 1; bool c = true; c ? ++a, ++b : --a, --b; cout << a << " " << b << endl;
What do you think this code prints? When you’ve thought of an answer, click on the below snippet to check the output:
2 1
The comma operator has a lower precedence than the ternary operator, and the expression is therefore parsed as if it was parenthesised like this:
(c ? ++a, ++b : --a), --b;
So b
is decremented no matter what the value of c
is.
Back to our code of appending values to a vector, we now understand that the code is parsed like this:
std::vector<int> v; ((((((((((v += 1),2),3),4),5),6),7),8),9),10);
Which is handy for us, because we won’t have to override the comma operator for all types.
The code of Boost.Assign is here. What follows is a slightly adapted version for two reasons:
- we will only implement the code of appending single values to a vector or set (Boost.Assign does many other things, which we will explore in future articles)
- Boost.Assign is implemented in C++98, and we’ll take advantage of modern C++ features to simplify the code
The general idea of the implementation is that operator+=
takes a vector and a value, appends that value to the vector, and returns an object that supports an operator,
that can a value to the vector.
Implementation of operator+=
The operator+=
we need to implement takes a vector and a value. It would have been nice to put it in namespace std
to benefit from the ADL, but C++ forbids that (doing so is undefined behaviour). We have to put it into a custom namespace, such as boost::assign
. This is why the client code has using namespace boost::assign
to bring operator+=
into scope.
Let’s focus on what operator+=
returns. It should be a custom type, able to add values to the vector with its operator,
. The implementation in Boost calls this object list_inserter
.
list_inserter
has to know how to add an object to the collection. Since it has to work on vectors (that add objects with .push_back
) as well as sets (that add objects with .insert
), the insertion of an element is a policy of list_inserter
, that is to say a template parameter focused on one aspect of the implementation (adding an element, here).
The policy that adds elements to a vector is called call_push_back
. Before getting into its own implementation, we can write operator+=
:
template<typename T, typename U> auto operator+=(std::vector<T>& container, U const& value) { return list_inserter(call_push_back(container)), value; }
A few implementation remarks:
value
has typeU
, which may be different from the typeT
of the elements of the vector. This is to deal with the case whereT
allows implicit conversions fromU
. Indeed, as we saw in the case of multiple types instd::max
, there is no implicit conversion with template arguments.- as we’ll see further down,
list_inserter
andcall_push_back
are template classes. Here we use C++17 type deduction in template class constructors to avoid burdening the code with template types that don’t add information. - the function returns
auto
, because the return type is cumbersome (it’s a template of a template). But maybe writing out the complete type would have made the code easier to understand? What do you think? - we know that we will use
list_inserter
later with anoperator,
to append values. We might as well start using it now, which is why the statement ends with,value
.
Implementation of operator,
We want list_inserter
to be callable on operator,
to perform an insertion by calling its inserter policy:
template<typename Inserter> class list_inserter { public: explicit list_inserter(Inserter inserter) : inserter_(inserter) {} template<typename T> list_inserter& operator,(T const& value) { inserter_(value); return *this; } private: Inserter inserter_; };
We need to invoke the inserter somehow. We could have given it an insert
method, but writing inserter_.insert
is redundant, so we go for operator()
.
Note that operator,
returns *this
. This allows to chain the calls to operator,
and append several elements successively.
The only thing left to implement is the policy, that binds to a container and adds a value to it:
template<typename Container> struct call_push_back { public: explicit call_push_back(Container& container) : container_(container) {} template<typename T> void operator()(T const& value) { container_.push_back(value); } private: Container& container_; };
Here is all the code put together:
#include <iostream> #include <vector> template<typename Inserter> class list_inserter { public: explicit list_inserter(Inserter inserter) : inserter_(inserter) {} template<typename T> list_inserter& operator,(T const& value) { inserter_(value); return *this; } private: Inserter inserter_; }; template<typename Container> struct call_push_back { public: explicit call_push_back(Container& container) : container_(container) {} template<typename T> void operator()(T const& value) { container_.push_back(value); } private: Container& container_; }; template<typename T, typename U> auto operator+=(std::vector<T>& container, U const& value) { return list_inserter(call_push_back(container)), value; } int main() { std::vector<int> v; v += 1,2,3,4,5,6,7,8,9,10; for (auto i : v) std::cout << i << ' '; }
To adapt it to a set
, we need to make an operator+=
that accepts a set, and a inserter policy that calls .insert
instead of .push_back
:
#include <iostream> #include <set> template<typename Inserter> class list_inserter { public: explicit list_inserter(Inserter inserter) : inserter_(inserter) {} template<typename T> list_inserter& operator,(T const& value) { inserter_(value); return *this; } private: Inserter inserter_; }; template<typename Container> struct call_insert { public: explicit call_insert(Container& container) : container_(container) {} template<typename T> void operator()(T const& value) { container_.insert(value); } private: Container& container_; }; template<typename T, typename U> auto operator+=(std::set<T>& container, U const& value) { return list_inserter(call_insert(container)), value; } int main() { std::set<int> s; s += 1,2,3,4,5,6,7,8,9,10; for (auto i : s) std::cout << i << ' '; }
There is more to Boost.Assign
This was a simplified implementation, because Boost.Assign has many more interesting features to add elements to a collection with expressive code. We will explore them in future articles.
You will also like
- Getting Along With The Comma Operator in C++
- What Books to Read to Get Better In C++
- How to Handle Multiple Types in Max Without A Cast
Share this post!