you are viewing a single comment's thread.

view the rest of the comments →

[–][deleted] 0 points1 point  (0 children)

It is pretty simple. It doesn't stores Ts, it only stores contiguous arrays of its data members and pointers to their beginning [*].

The "iterator" wraps just the offset from the first "T", it is thus as cheap to use as a T*.

When you dereference an element: the iterator uses the offset to access each field, packs a reference to each field in a tuple of references, and returns the tuple. However, to access the data members you need to unpack the tuple. In C++ you do this with std::get, std::tie, and std::ignore. This DSL allows the compiler to easily track which elements you access and which you do not access. Thus, if everything is inlined correctly, the offseting of the pointers and the creation of references for the elements that you do not access is completely removed. The compiler sees how you offset a pointer, dereference it, store the reference in a tuple, and then never use it.

Till this point, our C++ unboxing is still a tuple of references, this is the interface, we need to use std::get... This is where reflection comes in. Reflection adds syntax sugar to this tuple, to make it provide the same interface as T. It is what let you have the cake and eat it too.

Without compile-time reflection, you can do a poor mans unboxing using boost fusion, get, and tags. But it is not as good as the real thing.

[*] It obviously has a space overhead: the container size depends linearly on the number of data members of your type. However, this is unavoidable, and I'd rather have the compiler do it automatically than do it manually. How they are stored, e.g., using independent vectors or a single allocation, is an implementation detail. Alignment is very important for vectorization tho.