Tuple Access via Placeholder Names
12 May 2020The elements of a std::tuple
tp
are accessed by get<0>(tp)
, get<1>(tp)
,
and so on. A tuple is just an anonymous struct, so it would have been kind of
nice if we could use tp._1
, tp._2
, and so on, instead. (Since elements
have no names, we have to synthesize some. The Circle
language does something similar, except it (erroneously) starts from _0
.)
We can’t implement tp._1
because operator.
is not overloadable. We can’t
implement tp->_1
because even though operator->
is overloadable, it doesn’t
work for our needs; we can only modify the object on which the member access
occurs, not the field name.
What about tp.*_1
? Not overloadable, but wait, ->*
is. Can we use it?
We’ll need to define _1
, _2
and so on… or maybe not. The standard
already defines these for us, in the header <functional>
and the
namespace std::placeholders
. These are ordinarily used with std::bind
, but
nothing stops us from repurposing them.
We just need to define operator->*
which takes a std::tuple
on the left
side and a placeholder type on the right side… except that the placeholder
types are unspecified.
Fortunately, the person who designed std::bind
(that would be me) provided a
way to recognize whether a type is a placeholder type: the type trait
std::is_placeholder
. std::placeholder<T>::value
is 1 when T
is _1
, 2
when T
is _2
, 3 when T
is _3
… and 0 otherwise.
There we go:
#include <functional>
#include <tuple>
#include <type_traits>
template<class T, class U,
std::size_t I = std::size_t{std::is_placeholder<U>::value - 1},
std::size_t J = std::tuple_size<std::remove_reference_t<T>>::value>
decltype(auto) operator->*( T&& t, U ) noexcept
{
return std::get<I>( std::forward<T>( t ) );
}
Ignore the details, and this is pretty straightforward – t->*_1
returns std::get<1-1>(t)
.
The details are needed to constrain the operator so that it only applies to things that
are tuples on the left side, and things that are placeholders on the right side.
When U
is not a placeholder type, std::is_placeholder<U>::value
is 0, and
std::size_t{std::is_placeholder<U>::value - 1}
is std::size_t{-1}
. This is called a
narrowing conversion and is an error, specifically a “soft” error called a
substitution failure. The result is that the operator is not considered when U
is not
a placeholder, which is exactly what we want.
When T
is not a tuple (which means one of std::tuple
, std::pair
, std::array
),
std::tuple_size<T>::value
will not be defined, and we again get our desired substitution
failure.
Does this work?
#include <iostream>
int main()
{
using namespace std::placeholders;
auto t = std::make_tuple( 1, 2.0, "3" );
std::cout << t->*_1 << std::endl;
std::cout << t->*_2 << std::endl;
std::cout << t->*_3 << std::endl;
}