Imagine that you are creating a wrapper around some SQL database interface. You decided that boost::any
will perfectly match the requirements for a single cell of the database table.
Some other programmer will use your classes, and his/her task would be to get a row from the database and count the sum of the arithmetic types in a row.
This is what such a code would look like:
#include <boost/any.hpp> #include <vector> #include <string> #include <typeinfo> #include <algorithm> #include <iostream> // This typedefs and methods will be in our header, // that wraps around native SQL interface. typedef boost::any cell_t; typedef std::vector<cell_t> db_row_t; // This is just an example, no actual work with database. db_row_t get_row(const char* /*query*/) { // In real application 'query' parameter shall have a 'const // char*' or 'const std::string&' type? See recipe "Type // 'reference to string'" for an answer. db_row_t row; row.push_back(10); row.push_back(10.1f); row.push_back(std::string("hello again")); return row; } // This is how a user will use your classes struct db_sum { private: double& sum_; public: explicit db_sum(double& sum) : sum_(sum) {} void operator()(const cell_t& value) { const std::type_info& ti = value.type(); if (ti == typeid(int)) { sum_ += boost::any_cast<int>(value); } else if (ti == typeid(float)) { sum_ += boost::any_cast<float>(value); } } }; int main() { db_row_t row = get_row("Query: Give me some row, please."); double res = 0.0; std::for_each(row.begin(), row.end(), db_sum(res)); std::cout << "Sum of arithmetic types in database row is: " << res << std::endl; }
If you compile and run this example, it will output a correct answer:
Sum of arithmetic types in database row is: 20.1
Do you remember what your own thoughts were when reading the implementation of operator()
? I guess they were, "And what about double, long, short, unsigned, and other types?" The same thoughts will come into the head of a programmer who will use your interface. So, you need to carefully document values stored by your cell_t
or use a more elegant solution as described in the following sections.
Reading the previous two recipes is highly recommended if you are not already familiar with the Boost.Variant
and Boost.Any
libraries.
The Boost.Variant
library implements a visitor programming pattern for accessing the stored data, which is much safer than getting values via boost::get<>
. This pattern forces the programmer to take care of each type in variant, otherwise the code will fail to compile. You can use this pattern via the boost::apply_visitor
function, which takes a visitor
functional object as the first parameter and a variant
as the second parameter. If you are using a pre C++14 compiler, then visitor
functional objects must derive from the boost::static_visitor<T>
class, where T
is a type being returned by a visitor
. A visitor
object must have overloads of operator()
for each type stored by a variant.
Let's change the cell_t
type to boost::variant<int, float, string>
and modify our example:
#include <boost/variant.hpp> #include <vector> #include <string> #include <iostream> // This typedefs and methods will be in header, // that wraps around native SQL interface. typedef boost::variant<int, float, std::string> cell_t; typedef std::vector<cell_t> db_row_t; // This is just an example, no actual work with database. db_row_t get_row(const char* /*query*/) { // See recipe "Type 'reference to string'" // for a better type for 'query' parameter. db_row_t row; row.push_back(10); row.push_back(10.1f); row.push_back("hello again"); return row; } // This is a code required to sum values. // We can provide no template parameter // to boost::static_visitor<> if our visitor returns nothing. struct db_sum_visitor: public boost::static_visitor<double> { double operator()(int value) const { return value; } double operator()(float value) const { return value; } double operator()(const std::string& /*value*/) const { return 0.0; } }; int main() { db_row_t row = get_row("Query: Give me some row, please."); double res = 0.0; for (auto it = row.begin(), end = row.end(); it != end; ++it) { res += boost::apply_visitor(db_sum_visitor(), *it); } std::cout << "Sum of arithmetic types in database row is: " << res << std::endl; }
At compile time, the Boost.Variant
library generates a big switch
statement, each case of which calls a visitor
for a single type from the variant's list of types. At runtime, the index of the stored type is retrieved using which()
and jumps to a correct case inswitch
statement is made. Something like this will be generated for boost::variant<int, float, std::string>
:
switch (which()) { case 0 /*int*/: return visitor(*reinterpret_cast<int*>(address())); case 1 /*float*/: return visitor(*reinterpret_cast<float*>(address())); case 2 /*std::string*/: return visitor(*reinterpret_cast<std::string*>(address())); default: assert(false); }
Here, the address()
function returns a pointer to the internal storage of boost::variant<int, float, std::string>
.
If we compare this example with the first example in this recipe, we'll see the following advantages of boost::variant
:
- We know what types a variable can store
- If a library writer of the SQL interface adds or modifies a type held by a
variant
, we'll get a compile-time error instead of incorrect behavior
std::variant
from C++17 also supports visitation. Just write std::visit
instead of boost::apply_visitor
and you're done.
Note
You can download the example code files for all Packt books that you have purchased from your account at http://www.PacktPub.com. If you purchased this book elsewhere, you can visit http://www.PacktPub.com/support, and register to have the files emailed directly to you.
- After reading some recipes from Chapter 4, Compile-Time Tricks, you'll be able to make generic
visitor
objects that work correctly even if underlying types change - Boost's official documentation contains more examples and a description of some other features of
Boost.Variant
; it is available at the following link: http://boost.org/libs/variant