Whenever we use data structures that are compile time restricted in terms of extends, we allow for significant optimizations and early error detection in the expense of more cumbersome access style and code maintenance, due to the inevitable introduction of templated code. Users of such data types, algebraic (tuple, pair) or aggregate (array), breathed a sigh of relief (well… maybe not all of them) with the introduction of
std::apply in C++17, that handles the boilerplate of unpacking their contents into a callable. To give an example, you should now be able to write your callable like so
- Remain blissfully unaware of the structure that packs your arguments (the back end doesn’t need to specialize for different argument sources)
- Use the callable with any of the aforementioned data types without special treatment (the front end needs no boilerplate to unpack the data structures)
So, for example, you can know invoke your callable like so
2. Creating slices
Handy as it may be, this ecosystem of data types and tools, leaves a lot to be desired. First and foremost, the slice operation i.e. the creation of an entity that captures a subset of some given data. My main motivation for this was to be able to pass slices to the apply function although while researching the topic I found an old freestanding request, which I’ve just answered here (isn’t it a bummer when you invent something only to discover other people have had similar musings?) Below we define the tuple_slice operation that fills this gap and elaborate on the code:
Not to be confused with valarray’s slice, this piece of code does 3 simple things:
- Protects from invalid instantiations with static asserts
- Forwards the container (tuple, array, pair) + an integer sequence to the implementation function
- The implementation function gets every element indexed by the sequence and forwards it as a tuple i.e. lvalues are referenced and rvalues are move constructed
A slice of
t can be obtained like so
[I1, I2) is the exclusive range of the subset.
To be able to create slices that are not zero-based, all we do is create the index sequence out of the cardinality of the desired sequence and then offset the values. This works but a helper “make_integer_range” type would be nice to have:
You can experiment with integer ranges here. Back to the main act then (using
make_index_range to simplify the definition of
tuple_slice is trivial so we’ll omit it), here are some example uses of
3. Design considerations
One might ask “Shouldn’t a slice operation on an array create another array?” and this brings us to a generic code paradox: While trying to handle more cases we may end up being more restrictive; The extra effort to create respective
forward_as_array functions is not the problem (there you go if you don’t believe me). Here are the considerations:
- We cannot create an array of references, hence we’d fall back to creating an array of reference wrappers. Not only does this create an interface mismatch with the tuple case (we need to call
get()on a reference wrapper to obtain the value) but between lvalue and rvalue arrays as well: slicing the later would give us move constructed values, hence no
- How would this logic extend to the pair case? Slicing a pair does not give us yet another pair (only in the dummy case where the slice is identical to the original) so the fallback of producing a tuple would be viewed as an exception. To me, the frequency of the word “exception” in the description of a design is inversely proportional to its quality.
- Slices should handle uniformly the empty case and tuples do this .
- If there’s a unique sliced type, pattern matching it would require minimum effort.
- There’s always the option to create arrays out of the slices if we want to (would be interesting to see if the intermediate tuple can be optimized away) .
So, there you go … less is more