You can extend C++ with named operators that are "quoted" by standard C++ operators.
First we start with a dozen-line library:
namespace named_operator {
template<class D>struct make_operator{constexpr make_operator(){}};
template<class T, char, class O> struct half_apply { T&& lhs; };
template<class Lhs, class Op>
half_apply<Lhs, '*', Op> operator*( Lhs&& lhs, make_operator<Op> ) {
return {std::forward<Lhs>(lhs)};
}
template<class Lhs, class Op, class Rhs>
auto operator*( half_apply<Lhs, '*', Op>&& lhs, Rhs&& rhs )
-> decltype( named_invoke( std::forward<Lhs>(lhs.lhs), Op{}, std::forward<Rhs>(rhs) ) )
{
return named_invoke( std::forward<Lhs>(lhs.lhs), Op{}, std::forward<Rhs>(rhs) );
}
}
this doesn't do anything yet.
First, appending vectors
namespace my_ns {
struct append_t : named_operator::make_operator<append_t> {};
constexpr append_t append{};
template<class T, class A0, class A1>
std::vector<T, A0> named_invoke( std::vector<T, A0> lhs, append_t, std::vector<T, A1> const& rhs ) {
lhs.insert( lhs.end(), rhs.begin(), rhs.end() );
return std::move(lhs);
}
}
using my_ns::append;
std::vector<int> a {1,2,3};
std::vector<int> b {4,5,6};
auto c = a *append* b;
The core here is that we define an append
object of type append_t:named_operator::make_operator<append_t>
.
We then overload named_invoke( lhs, append_t, rhs ) for the types we want on the right and left.
The library overloads lhs*append_t
, returning a temporary half_apply
object. It also overloads half_apply*rhs
to call named_invoke( lhs, append_t, rhs )
.
We simply have to create the proper append_t
token and do an ADL-friendly named_invoke
of the proper signature, and everything hooks up and works.
For a more complex example, suppose you want to have element-wise multiplication of elements of a std::array:
template<class=void, std::size_t...Is>
auto indexer( std::index_sequence<Is...> ) {
return [](auto&& f) {
return f( std::integral_constant<std::size_t, Is>{}... );
};
}
template<std::size_t N>
auto indexer() { return indexer( std::make_index_sequence<N>{} ); }
namespace my_ns {
struct e_times_t : named_operator::make_operator<e_times_t> {};
constexpr e_times_t e_times{};
template<class L, class R, std::size_t N,
class Out=std::decay_t<decltype( std::declval<L const&>()*std::declval<R const&>() )>
>
std::array<Out, N> named_invoke( std::array<L, N> const& lhs, e_times_t, std::array<R, N> const& rhs ) {
using result_type = std::array<Out, N>;
auto index_over_N = indexer<N>();
return index_over_N([&](auto...is)->result_type {
return {{
(lhs[is] * rhs[is])...
}};
});
}
}
This element-wise array code can be extended to work on tuples or pairs or C-style arrays, or even variable length containers if you decide what to do if the lengths don't match.
You could also an element-wise operator type and get lhs *element_wise<'+'>* rhs
.
Writing a *dot*
and *cross*
product operators are also obvious uses.
The use of *
can be extended to support other delimiters, like +
. The delimeter precidence determines the precidence of the named operator, which may be important when translating physics equations over to C++ with minimal use of extra ()
s.
With a slight change in the library above, we can support ->*then*
operators and extend std::function
prior to the standard being updated, or write monadic ->*bind*
. It could also have a stateful named operator, where we carefully pass the Op
down to the final invoke function, permitting:
named_operator<'*'> append = [](auto lhs, auto&& rhs) {
using std::begin; using std::end;
lhs.insert( end(lhs), begin(rhs), end(rhs) );
return std::move(lhs);
};
generating a named container-appending operator in C++17.