Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
CPlusPlusNotesForProfessionals.pdf
Скачиваний:
47
Добавлен:
20.05.2023
Размер:
5.11 Mб
Скачать

auto d7 = 1 / a;

// All of these are complex<double> auto e0 = b + 1;

auto e1 = 1 + b; auto e2 = b - 1; auto e3 = 1 - b; auto e4 = b * 1; auto e5 = 1 * b; auto e6 = b / 1; auto e7 = 1 / b;

return 0;

}

Section 36.5: Named operators

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};

GoalKicker.com – C++ Notes for Professionals

206

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])...

}}; });

}

}

live example.

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:

GoalKicker.com – C++ Notes for Professionals

207

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.

Section 36.6: Unary operators

You can overload the 2 unary operators:

++foo and foo++

--foo and foo--

Overloading is the same for both types (++ and --). Scroll down for explanation

Overloading outside of class/struct:

//Prefix operator ++foo T& operator++(T& lhs)

{

//Perform addition return lhs;

}

//Postfix operator foo++ (int argument is used to separate preand postfix) //Should be implemented in terms of ++foo (prefix operator)

T operator++(T& lhs, int)

{

T t(lhs); ++lhs; return t;

}

Overloading inside of class/struct:

//Prefix operator ++foo T& operator++()

{

//Perform addition return *this;

}

//Postfix operator foo++ (int argument is used to separate preand postfix) //Should be implemented in terms of ++foo (prefix operator)

T operator++(int)

{

T t(*this); ++(*this); return t;

}

Note: The prefix operator returns a reference to itself, so that you can continue operations on it. The first argument is a reference, as the prefix operator changes the object, that's also the reason why it isn't const (you wouldn't be able to modify it otherwise).

GoalKicker.com – C++ Notes for Professionals

208

The postfix operator returns by value a temporary (the previous value), and so it cannot be a reference, as it would be a reference to a temporary, which would be garbage value at the end of the function, because the temporary variable goes out of scope). It also cannot be const, because you should be able to modify it directly.

The first argument is a non-const reference to the "calling" object, because if it were const, you wouldn't be able to modify it, and if it weren't a reference, you wouldn't change the original value.

It is because of the copying needed in postfix operator overloads that it's better to make it a habit to use prefix ++ instead of postfix ++ in for loops. From the for loop perspective, they're usually functionally equivalent, but there might be a slight performance advantage to using prefix ++, especially with "fat" classes with a lot of members to copy. Example of using prefix ++ in a for loop:

for (list<string>::const_iterator it = tokens.begin(); it != tokens.end();

++it) { // Don't use it++

...

}

Section 36.7: Comparison operators

You can overload all comparison operators:

== and !=

> and <

>= and <=

The recommended way to overload all those operators is by implementing only 2 operators (== and <) and then using those to define the rest. Scroll down for explanation

Overloading outside of class/struct:

//Only implement those 2

bool operator==(const T& lhs, const T& rhs) { /* Compare */ } bool operator<(const T& lhs, const T& rhs) { /* Compare */ }

//Now you can define the rest

bool operator!=(const T& lhs, const T& rhs) { return !(lhs == rhs); } bool operator>(const T& lhs, const T& rhs) { return rhs < lhs; } bool operator<=(const T& lhs, const T& rhs) { return !(lhs > rhs); } bool operator>=(const T& lhs, const T& rhs) { return !(lhs < rhs); }

Overloading inside of class/struct:

//Note that the functions are const, because if they are not const, you wouldn't be able //to call them if the object is const

//Only implement those 2

bool operator==(const T& rhs) const { /* Compare */ } bool operator<(const T& rhs) const { /* Compare */ }

//Now you can define the rest

bool operator!=(const T& rhs) const { return !(*this == rhs); } bool operator>(const T& rhs) const { return rhs < *this; } bool operator<=(const T& rhs) const { return !(*this > rhs); } bool operator>=(const T& rhs) const { return !(*this < rhs); }

GoalKicker.com – C++ Notes for Professionals

209