Note that if one understood the article up to the definition of ConstructT, then feel free to skip to that section.
Update: I have attempted to improve the original article with some of this one, so it may be a little redundant.
Binary
Binary defines member functions for partially applying the first and last arguments. I wrote about this back in September.template< class F, class X > struct Part { F f; X x; template< class _F, class _X > constexpr Part( _F&& f, _X&& x ) : f(forward<_F>(f)), x(forward<_X>(x)) { } template< class ... Xs > constexpr auto operator () ( Xs&& ...xs ) -> decltype( f(x,declval<Xs>()...) ) { return f( x, forward<Xs>(xs)... ); } }; template< class F, class X > struct RPart { F f; X x; template< class _F, class _X > constexpr RPart( _F&& f, _X&& x ) : f( forward<_F>(f) ), x( forward<_X>(x) ) { } template< class ...Y > constexpr decltype( f(declval<Y>()..., x) ) operator() ( Y&& ...y ) { return f( forward<Y>(y)..., x ); } }; template< class F > struct Binary { template< class X > constexpr Part<F,X> operator () ( X x ) { return Part<F,X>( F(), move(x) ); } template< class X > constexpr RPart<F,X> with( X x ) { return RPart<F,X>( F(), move(x) ); } };
So, given some type, F, inherited from Binary<F>, F is defined as taking one argument (partial application), and calling the member function with (reverse-partial application). F must be a type that defines a two-argument operator() overload.
We define a derivation like so:
constexpr struct Add : public Binary<Add> { using Binary<Add>::operator(); template< class X, class Y > constexpr auto operator () ( X&& x, Y&& y ) -> decltype( std::declval<X>() + std::declval<Y>() ) { return std::forward<X>(x) + std::forward<Y>(y); } } add{};
Add, is not a function in the C sense, it's a function type. add is an instance of Add and is a function object. Add does not inherit Binary's operator() overload by default, so we explicitly tell it to by saying "using Binary<Add>::operator()". Binary's with requires no work to inherit.
auto inc = add(1); // Calls Binary<Add>::operator(); returns Part<Add,int>.
auto dec = add.with(-1); // Calls Binary<Add>::with; returns RPart<Add,int>.
int two = inc(1); // Calls Part<Add,int>::operator(); returns add(1,1).
int one = dec(two); // returns add(2,-1).
add(1,2) -- Calls Add::operator(); returns int(3).
Chainable.
template< class F > struct Chainable : Binary<F> { using Binary<F>::operator(); template< class X, class Y > using R = typename std::result_of< F(X,Y) >::type; // Three arguments: unroll. template< class X, class Y, class Z > constexpr auto operator () ( X&& x, Y&& y, Z&& z ) -> R< R<X,Y>, Z > { return F()( F()( std::forward<X>(x), std::forward<Y>(y) ), std::forward<Z>(z) ); } template< class X, class Y, class ...Z > using Unroll = typename std::result_of < Chainable<F>( typename std::result_of<F(X,Y)>, Z... ) >::type; // Any more? recurse. template< class X, class Y, class Z, class H, class ...J > constexpr auto operator () ( X&& x, Y&& y, Z&& z, H&& h, J&& ...j ) -> Unroll<X,Y,Z,H,J...> { // Notice how (*this) always gets applied at LEAST three arguments. return (*this)( F()( std::forward<X>(x), std::forward<Y>(y) ), std::forward<Z>(z), std::forward<H>(h), std::forward<J>(j)... ); } };
Chainable works in much the same way as Binary does, except that it extends F to take any arbitrary number of arguments (except for zero, but that'd be pretty useless anyway). Redefining Add to be Chainable is not hard.
constexpr struct Add : public Chainable<Add> { using Chainable<Add>::operator(); template< class X, class Y > constexpr auto operator () ( X&& x, Y&& y ) -> decltype( std::declval<X>() + std::declval<Y>() ) { return std::forward<X>(x) + std::forward<Y>(y); } } add{};
Only two lines have changed, however add's behaviour has changed dramatically.
int x = add(1,2,3); // Calls Chainable<Add>::operator(X,Y,Z); returns (1+2)+3.
int y = add(1,2,3,4,5,6); // Calls Chainable<Add>::operator(X,Y,Z,H,J...).
auto inc = add(1); // Still calls Binary<Add>::operator().
The three arg version of Chainable<Add> calls add( add(x,y), z ), while the variadic version calls itself( add(x,y), z, h, j... ). It reduces the number of arguments to process by one, being supplied at least four. It is therefore never the base case and always ends by calling its three-arg version.
ConstructT.
template< template<class...> class T > struct ConstructT { template< class ...X, class R = T< typename std::decay<X>::type... > > constexpr R operator () ( X&& ...x ) { return R( forward<X>(x)... ); } };
The first template argument may be confusing to those who have not seen them before. We could write template<class> class T, and that would expect a T that took one type parameter. It may actually take one, zero, or several. template<class...> class T is the generic way to say "we know T takes type parameters, but we don't know how many (and it doesn't yet matter)". It might be std::vector, which can take two; the value type and allocator. It might be std::set, which can take three. (Though, neither vector nor set would work for ConstructT.)
We deduce the return type using the default template parameter, R. std::decay transforms the type such that if we pass in an int&, we get an int back.
typename std::decay<const int&>::type = int
typename std::decay<int&&>::type = int
typename std::decay<int>::type = int
So, if T=std::pair and we pass in an int& and std::string&&, R = std::pair<int,std::string>.
ConstructT perfectly forwards the arguments to T's constructor, but decays the types to ensure that it holds the actual values.
Just the same as with add and Add, we must create an instance of ConstructT to call it.
constexpr auto make_pair = ConstructT<std::pair>();
This version of make_pair behaves just like std::make_pair. Its type is equivalent to this:
template< struct ConstructPair { template< class ...X, class R = std::pair< typename std::decay<X>::type... > > constexpr R operator () ( X&& ...x ) { return R( forward<X>(x)... ); } };
I've used several similar constructs in the past. Generic function objects support a clear and concise functional style, and leave the mess to the compiler.
ReplyDeleteContrast these with function objects that happen to be generic classes, e.g., std::plus, std::minus, etc... These are a pain to use, at best, lacking generic invocation.
Heello nice blog
ReplyDelete