...one of the most highly
regarded and expertly designed C++ library projects in the
world.
— Herb Sutter and Andrei
Alexandrescu, C++
Coding Standards
std::tuple
and std::pair
are good for generic programming, however they have disadvantages. First
of all, code that uses them becomes barely readable. Consider two definitions:
Tuple |
Aggregate |
---|---|
using auth_info_tuple = std::tuple< std::int64_t, // What does this integer represents? std::int64_t, std::time_t >; |
struct auth_info_aggregate { std::int64_t user_id; // Oh, now I see! std::int64_t session_id; std::time_t valid_till; }; |
Definition via aggregate initializable structure is much more clear. Same
story with usages: return std::get<1>(value);
vs. return
value.session_id;
.
Another advantage of aggregates is a more efficient copy, move construction and assignments.
Because of the above issues some guidelines recommend to use aggregates instead of tuples. However aggregates fail when it comes to the functional like programming.
Boost.PFR library provides tuple like methods for aggregate initializable structures, making aggregates usable in contexts where only tuples were useful.
The following example shows how to access structure fields by index using
boost::pfr::get
.
Let's define some structure:
#include <boost/pfr/core.hpp> struct foo { // defining structure int some_integer; char c; };
We can access fields of that structure by index:
foo f {777, '!'}; auto& r1 = boost::pfr::get<0>(f); // accessing field with index 0, returns reference to `foo::some_integer` auto& r2 = boost::pfr::get<1>(f); // accessing field with index 1, returns reference to `foo::c`
The following example shows how to write your own io-manipulator for printing:
#include <boost/pfr/ops.hpp> #include <ostream> namespace my_ns { /// Usage: /// struct foo {std::uint8_t a, b;}; /// ... /// std::cout << my_ns::my_io(foo{42, 22}); /// /// Output: 42, 22 template <class T> auto my_io(const T& value); namespace detail { // Helpers to print individual values template <class T> void print_each(std::ostream& out, const T& v) { out << v; } void print_each(std::ostream& out, std::uint8_t v) { out << static_cast<unsigned>(v); } void print_each(std::ostream& out, std::int8_t v) { out << static_cast<int>(v); } // Structure to keep a reference to value, that will be ostreamed lower template <class T> struct io_reference { const T& value; }; // Output each field of io_reference::value template <class T> std::ostream& operator<<(std::ostream& out, io_reference<T>&& x) { const char* sep = ""; boost::pfr::for_each_field(x.value, [&](const auto& v) { out << std::exchange(sep, ", "); detail::print_each(out, v); }); return out; } } // Definition: template <class T> auto my_io(const T& value) { return detail::io_reference<T>{value}; } } // namespace my_ns
There are three ways to start using Boost.PFR hashing, comparison and streaming
for type T
in your code.
Each method has its own drawbacks and suits own cases.
Table 28.1. Different approaches for operators
Approach |
When to use |
Operators could be found by ADL |
Works for local types |
Usable locally, without affecting code from other scopes |
Ignores implicit conversion operators |
Respects user defined operators |
---|---|---|---|---|---|---|
Use when you need to compare values by provided for them operators or via field-by-field comparison. |
no |
yes |
yes |
no |
yes |
|
Use near the type definition to define the whole set of operators for your type. |
yes |
no |
no |
yes for T |
no (compile time error) |
|
|
Use to implement the required set of operators for your type. |
no |
yes |
yes |
yes |
yes |
More detailed description follows:
1. eq, ne,
gt, lt, le, ge,
io
approach
This method is good if you're writing generic algorithms and need to use operators from Boost.PFR only if there are no operators defined for the type:
#include <boost/pfr/ops.hpp> template <class T> struct uniform_comparator_less { bool operator()(const T& lhs, const T& rhs) const noexcept { // If T has operator< or conversion operator then it is used. return boost::pfr::lt(lhs, rhs); } };
This methods effects are local to the function. It works even for local types, like structures defined in functions.
2. BOOST_PFR_FUNCTIONS_FOR(T) approach
This method is good if you're writing a structure and wish to define operators for that structure.
#include <boost/pfr/functions_for.hpp> struct pair_like { int first; short second; }; BOOST_PFR_FUNCTIONS_FOR(pair_like) // Defines operators // ... assert(pair_like{1, 2} < pair_like{1, 3});
Argument Dependant Lookup works well. std::less
will find the operators for struct
pair_like
. BOOST_PFR_FUNCTIONS_FOR(T)
can not be used for local types. It does not respect conversion operators
of T
, so for example the
following code will output different values:
#include <boost/pfr/functions_for.hpp> struct empty { operator std::string() { return "empty{}"; } }; // Uncomment to get different output: // BOOST_PFR_FUNCTIONS_FOR(empty) // ... std::cout << empty{}; // Outputs `empty{}` if BOOST_PFR_FUNCTIONS_FOR(empty) is commented out, '{}' otherwise.
3. eq_fields, ne_fields, gt_fields, lt_fields, le_fields, ge_fields, io_fields
approach
This method is good if you're willing to provide only some operators for your type:
#include <boost/pfr/io_fields.hpp> struct pair_like { int first; std::string second; }; inline std::ostream& operator<<(std::ostream& os, const pair_like& x) { return os << bost::pfr::io_fields(x); }
All the *_fields
functions do ignore user defined operators and work only with fields of a
type. This makes them perfect for defining you own operators.
You could use tuple-like representation if a type contains union. But be sure that operations for union are manually defined:
#include <boost/pfr/ops.hpp> union test_union { int i; float f; }; inline bool operator==(test_union l, test_union r) noexcept; // Compile time error without this operator bool some_function(test_union f1, test_union f2) { return boost::pfr::eq(f1, f2); // OK }
Reflection of unions is disabled in the Boost.PFR library for safety reasons.
Alas, there's no way to find out active
member of a union and accessing an inactive member is an Undefined Behavior.
For example, library could always return the first member, but ostreaming
u
in union
{char* c;
long long
ll; } u;
u.ll= 1;
will crash your program with an invalid
pointer dereference.
Any attempt to reflect unions leads to a compile time error. In many cases a static assert is triggered that outputs the following message:
error: static_assert failed "====================> Boost.PFR: For safety reasons it is forbidden to reflect unions. See `Reflection of unions` section in the docs for more info."