Skip to content

rfl::NamedTuple

rfl::NamedTuple is very similar to std::tuple or rfl::Tuple, but unlike these two structures, the fields have names.

In other words, consider the following struct:

struct Person {
    std::string first_name;
    std::string last_name;
    rfl::Timestamp<"%Y-%m-%d"> birthday;
};

You might as well define the following rfl::NamedTuple:

using Person = rfl::NamedTuple<
    rfl::Field<"first_name", std::string>,
    rfl::Field<"last_name", std::string>,
    rfl::Field<"birthday", rfl::Timestamp<"%Y-%m-%d">>>;

From the point-of-view of serialization/deserialization, the two definitions are equivalent. The resulting JSON strings (or any other format) will be the same.

Structural typing

From the point-of-view of programming, there is an important difference: Structs are nominally typed and named tuples are structurally typed (confusingly, structs are not structurally typed).

In plain language, that means that the compiler will regard this as absolutely equivalent to Person, the named tuple:

using Person2 = rfl::NamedTuple<
    rfl::Field<"first_name", std::string>,
    rfl::Field<"last_name", std::string>,
    rfl::Field<"birthday", rfl::Timestamp<"%Y-%m-%d">>>;

However, this will be seen as a type that is different from Person, the struct:

struct Person2 {
    std::string first_name;
    std::string last_name;
    rfl::Timestamp<"%Y-%m-%d"> birthday;
};

Structural typing also means that you can declare new types on-the-fly. For instance, in order to create a Person named tuple, you don't actually have to declare it at all. The following will do:

const auto person = rfl::Field<"first_name", std::string>("Homer") *
                    rfl::Field<"last_name", std::string>("Simpson") *
                    rfl::Field<"birthday", rfl::Timestamp<"%Y-%m-%d">>("1987-04-19");

The type of person will now be equivalent to the definition of Person, the named tuple, regardless of whether you have actually declared it anywhere.

On the other hand, structural typing also means that recursive definitions are impossible. For instance, consider something like this:

struct Person {
    std::string first_name;
    std::string last_name;
    std::vector<Person> children;
};

In this example, Person is recursively defined (because of the field children). This is impossible to accomplish using structural typing and rfl::NamedTuple, just like it is impossible to have a recursively defined lambda function.

Accessing fields

Fields inside the named tuple can be accessed using rfl::get or the .get method:

const auto person = rfl::Field<"first_name", std::string>("Homer") *
                    rfl::Field<"last_name", std::string>("Simpson") *
                    rfl::Field<"birthday", rfl::Timestamp<"%Y-%m-%d">>("1987-04-19");

// OK in most circumstances (there are restrictions
// due to the way C++ templates work).
const auto first_name = person.get<"firstName">();

// Always OK
const auto first_name = person.template get<"firstName">();

// Always OK
const auto first_name = rfl::get<"firstName">(person);

Fields can also be iterated over at compile-time using the apply() method:

auto person = rfl::Field<"first_name", std::string>("Bart") *
              rfl::Field<"last_name", std::string>("Simpson");

person.apply([](const auto& f) {
  auto field_name = f.name();
  const auto& value = *f.value();
});

person.apply([]<typename Field>(Field& f) {
  // The field name can also be obtained as a compile-time constant.
  constexpr auto field_name = Field::name();
  using field_pointer_type = typename Field::Type;
  field_pointer_type* value = f.value();
});

Monadic operations: .transform and .and_then

Named tuples also contain compile-time monadic operations.

.transform(f) expects a function f of type Field -> Field. transform then applies that function to each field of the named tuple. It can be used to change either the values or the names of the fields, but not their overall number.

const auto lisa =
    Person{.first_name = "Lisa", .last_name = "Simpson", .age = 8};

const auto to_bart = [](auto field) {
if constexpr (decltype(field)::name() == "first_name") {
    field = "Bart";
    return field;
} else if constexpr (decltype(field)::name() == "age") {
    field = 10;
    return field;
} else {
    return field;
}
};

// bart will now be a named tuple with first_name="Bart",
// last_name="Simpson", age=10
const auto bart = rfl::to_named_tuple(lisa).transform(to_bart);

.and_then(f) expects a function f of type Field -> NamedTuple. and_then then applies that function to each field of the named tuple and finally concatenates the resulting named tuple to form a new named tuple. Note that the named tuple returned by f may be empty. .and_then(f) can be used to change either the values or the names of the fields, and can also affect their overall number.

const auto lisa =
    Person{.first_name = "Lisa", .last_name = "Simpson", .age = 8};

const auto to_bart = [](auto field) {
if constexpr (decltype(field)::name() == "first_name") {
    field = "Bart";
    return rfl::make_named_tuple(field);
} else if constexpr (decltype(field)::name() == "age") {
    return rfl::make_named_tuple();
} else {
    return rfl::make_named_tuple(field);
}
};

// bart will now be a named tuple with first_name="Bart",
// last_name="Simpson". Since we have returned and empty
// named tuple for the field "age", there will be no such
// field in bart.
const auto bart = rfl::to_named_tuple(lisa).and_then(to_bart);

rfl::replace

rfl::replace works for rfl::NamedTuple as well:

const auto lisa = rfl::Field<"firstName", std::string>("Lisa") *
                  rfl::Field<"lastName", std::string>("Simpson");

// Returns a deep copy of the original object,
// replacing first_name.
const auto maggie =
    rfl::replace(lisa, rfl::make_field<"firstName">(std::string("Maggie")));

// Also OK
const auto bart = lisa.replace(rfl::make_field<"firstName">(std::string("Bart")));

rfl::as

So does rfl::as:

using C = rfl::NamedTuple<
    rfl::Field<"f1", std::string>,
    rfl::Field<"f2", std::string>,
    rfl::Field<"f4", std::string>>;

const auto a = rfl::Field<"f1", std::string>("Hello") *  
               rfl::Field<"f2", std::string>("World");

const auto b = rfl::Field<"f3", std::string>("Hello") *  
               rfl::Field<"f4", std::string>("World");

const auto c = rfl::as<C>(a, b);

However, you do not really have to use rfl::as here. This will work as well:

// Same as rfl::as<C>(a, b)
const auto c = C(a * b);

(in fact, this is how rfl::as is implemented in the first place).

Defining named tuples using other named tuples

rfl::Flatten is not supported inside named tuples. Instead, you can use rfl::define_named_tuple_t<...> to achieve the same goal:

using Person = rfl::NamedTuple<
    rfl::Field<"firstName", std::string>,
    rfl::Field<"lastName", std::string>,
    rfl::Field<"age", int>>;

using Employee = rfl::define_named_tuple_t<
    Person,
    rfl::Field<"salary", float>>;

const auto employee = Employee(
    rfl::Field<"firstName", std::string>("Homer"),
    rfl::Field<"lastName", std::string>("Simpson"),
    rfl::make_field<"age">(45),
    rfl::make_field<"salary">(60000.0));

Transforming structs to named tuples and vice versa

You can transform structs to named tuples and vice versa (this will only work with the rfl::Field-syntax:

auto bart = Person{.first_name = "Bart",
                   .last_name = "Simpson",
                   .birthday = "1987-04-19"};

// bart_nt is a named tuple
const auto bart_nt = rfl::to_named_tuple(bart);

// You can also retrieve the equivalent named tuple
// type to a struct:
using PersonNamedTuple = rfl::named_tuple_t<Person>;

// rfl::to_named_tuple also supports move semantics
PersonNamedTuple bart_nt = rfl::to_named_tuple(std::move(bart_nt));

// You can also go the other way
const auto bart_struct = rfl::from_named_tuple<Person>(bart_nt);
const auto bart_struct = rfl::from_named_tuple<Person>(std::move(bart_nt));