Boost C++ Libraries Home Libraries People FAQ More

PrevUpHomeNext

Tutorial

Why tuples are bad and aggregates are more preferable?
Accessing structure member by index
Custom printing of aggregates
Three ways of getting operators
Reflection of unions
Reflection of field name

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 26.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

boost/pfr/ops.hpp: eq, ne, gt, lt, le, ge

boost/pfr/io.hpp: io

Use when you need to compare values by provided for them operators or via field-by-field comparison.

no

yes

yes

no

yes

BOOST_PFR_FUNCTIONS_FOR(T)

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)

boost/pfr/ops_fields.hpp: eq_fields, ne_fields, gt_fields, lt_fields, le_fields, ge_fields

boost/pfr/io_fields.hpp: io_fields

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."

Since C++20 it's possible to read name of a structure field by index using Boost.PFR library. The following example shows how to do it using boost::pfr::get_name.

Let's define some structure:

#include <boost/pfr/core_name.hpp>

struct foo {            // defining structure
    int some_integer;
    char c;
};

We can access field's names of that structure by index:

constexpr std::string_view n1 = boost::pfr::get_name<0, foo>(); // returns "some_integer"
constexpr std::string_view n2 = boost::pfr::get_name<1, foo>(); // returns "c"

See Limitations and Configuration.


PrevUpHomeNext