RecordRef
During a view accesses like view(1, 2, 3)(color{}, g{})
an intermediate object is needed for this to work.
This object is a llama::RecordRef
.
using Pixel = llama::Record<
llama::Field<color, llama::Record<
llama::Field<r, float>,
llama::Field<g, float>,
llama::Field<b, float>
>>,
llama::Field<alpha, char>
>;
// ...
auto vd = view(1, 2, 3);
vd(color{}, g{}) = 1.0;
// or:
auto vdColor = vd(color{});
float& g = vdColor(g{});
g = 1.0;
Supplying the array dimensions coordinate to a view access returns such a llama::RecordRef
, storing this array dimensions coordinate.
This object models a reference to a record in the \(N\)-dimensional array dimensions space,
but as the fields of this record may not be contiguous in memory, it is not a native l-value reference.
Accessing subparts of a llama::RecordRef
is done using operator()
and the tag types from the record dimension.
If an access describes a final/leaf element in the record dimension, a reference to a value of the corresponding type is returned.
Such an access is called terminal. If the access is non-terminal, i.e. it does not yet reach a leaf in the record dimension tree,
another llama::RecordRef
is returned, binding the tags already used for navigating down the record dimension.
A llama::RecordRef
can be used like a real local object in many places. It can be used as a local variable, copied around, passed as an argument to a function (as seen in the
nbody example), etc.
In general, llama::RecordRef
is a value type that represents a reference, similar to an iterator in C++ (llama::One
is a notable exception).
One
llama::One<RecordDim>
is a shortcut to create a scalar llama::RecordRef
.
This is useful when we want to have a single record instance e.g. as a local variable.
llama::One<Pixel> pixel;
pixel(color{}, g{}) = 1.0;
auto pixel2 = pixel; // independent copy
Technically, llama::One
is a llama::RecordRef
which stores a scalar llama::View
inside, using the mapping llama::mapping::One
.
This also has the consequence that a llama::One
is now a value type with deep-copy semantic.
Arithmetic and logical operators
llama::RecordRef
overloads several operators:
auto record1 = view(1, 2, 3);
auto record2 = view(3, 2, 1);
record1 += record2;
record1 *= 7.0; //for every element in the record dimension
foobar(record2);
//With this somewhere else:
template<typename RecordRef>
void foobar(RecordRef vr)
{
vr = 42;
}
The assignment operator ( =
) and the arithmetic, non-bitwise, compound assignment operators (=
, +=
, -=
, *=
, /=
, %=
) are overloaded.
These operators directly write into the corresponding view.
Furthermore, the binary, non-bitwise, arithmetic operators ( +
, -
, *
, /
, %
) are overloaded too,
but they return a temporary object on the stack (i.e. a llama::One
).
These operators work between two record references, even if they have different record dimensions. Every tag existing in both record dimensions will be matched and operated on. Every non-matching tag is ignored, e.g.
using RecordDim1 = llama::Record<
llama::Record<llama::Field<pos
llama::Field<x, float>
>>,
llama::Record<llama::Field<vel
llama::Field <x, double>
>>,
llama::Field <x, int>
>;
using RecordDim2 = llama::Record<
llama::Record<llama::Field<pos
llama::Field<x, double>
>>,
llama::Record<llama::Field<mom
llama::Field<x, double>
>>
>;
// Let assume record1 using RecordDim1 and record2 using RecordDim2.
record1 += record2;
// record2.pos.x will be added to record1.pos.x because
// of pos.x existing in both record dimensions although having different types.
record1(vel{}) *= record2(mom{});
// record2.mom.x will be multiplied to record2.vel.x as the first part of the
// record dimension coord is explicit given and the same afterwards
The discussed operators are also overloaded for types other than llama::RecordRef
as well so that
record1 *= 7.0
will multiply 7 to every element in the record dimension.
This feature should be used with caution!
The comparison operators ==
, !=
, <
, <=
, >
and >=
are overloaded too and return true
if
the operation is true for all pairs of fields with equal tag.
Let’s examine this deeper in an example:
using A = llama::Record <
llama::Field < x, float >,
llama::Field < y, float >
>;
using B = llama::Record<
llama::Field<z, double>,
llama::Field<x, double>
>;
bool result;
llama::One<A> a1, a2;
llama::One<B> b;
a1(x{}) = 0.0f;
a1(y{}) = 2.0f;
a2 = 1.0f; // sets x and y to 1.0f
b(x{}) = 1.0f;
b(z{}) = 2.0f;
result = a1 < a2;
//result is false, because a1.y > a2.y
result = a1 > a2;
//result is false, too, because now a1.x > a2.x
result = a1 != a2;
//result is true
result = a2 == b;
//result is true, because only the matching "x" matters
A partial addressing of a record reference like record1(color{}) *= 7.0
is also possible.
record1(color{})
itself returns a new record reference with the first record dimension coordinate (color
) being bound.
This enables e.g. to easily add a velocity to a position like this:
using Particle = llama::Record<
llama::Field<pos, llama::Record<
llama::Field<x, float>,
llama::Field<y, float>,
llama::Field<z, float>
>>,
llama::Field<vel, llama::Record<
llama::Field<x, double>,
llama::Field<y, double>,
llama::Field<z, double>
>>,
>;
// Let record be a record reference with the record dimension "Particle".
record(pos{}) += record(vel{});
Tuple interface
A struct in C++ can be modelled by a std::tuple
with the same types as the struct’s members.
A llama::RecordRef
behaves like a reference to a struct (i.e. the record) which is decomposed into it’s members.
We can therefore not form a single reference to such a record, but references to the individual members.
Organizing these references inside a std::tuple
in the same way the record is represented in the record dimension gives us an alternative to a llama::RecordRef
.
Mind that creating such a std::tuple
already invokes the mapping function, regardless of whether an actual memory access occurs through the constructed reference later.
However, such dead address computations are eliminated by most compilers during optimization.
auto record = view(1, 2, 3);
std::tuple<std::tuple<float&, float&, float&>, char&> = record.asTuple();
std::tuple<float&, float&, float&, char&> = record.asFlatTuple();
auto [r, g, b, a] = record.asFlatTuple();
Additionally, if the user already has types supporting the C++ tuple interface, llama::RecordRef
can integrate with these using the load()
, loadAs<T>()
and store(T)
functions.
struct MyPixel {
struct {
float r, g, b;
} color;
char alpha;
};
// implement std::tuple_size<MyPixel>, std::tuple_element<MyPixel> and get(MyPixel)
auto record = view(1, 2, 3);
MyPixel p1 = record.load(); // constructs MyPixel from 3 float& and 1 char&
auto p2 = record.loadAs<MyPixel>(); // same
p1.alpha = 255;
record.store(p1); // tuple-element-wise assignment from p1 to record.asFlatTuple()
Keep in mind that the load and store functionality always reads/writes all elements referred to by a llama::RecordRef
.
Structured bindings
A llama::RecordRef
implements the C++ tuple interface itself to allow destructuring:
auto record = view(1, 2, 3);
auto [color, a] = record; // color is another RecordRef, a is a char&, 1 call to mapping function
auto [r, g, b] = color; // r, g, b are float&, 3 calls to mapping function
Contrary to destructuring a tuple generated by calling asTuple()
or asFlatTuple()
,
the mapping function is not invoked for other instances of llama::RecordRef
created during the destructuring.
The mapping function is just invoked to form references for terminal accesses.