Document #: | P3171R0 |
Date: | 2024-03-04 |
Project: | Programming Language C++ |
Audience: |
LEWG |
Reply-to: |
Peter Dimov <[email protected]> Barry Revzin <[email protected]> |
As noted in [P2760R0], there are a lot of function objects for operators in the standard library, but several operators are missing. This paper proposes to add the functionality for all the missing operators, but to also do it in a different way than simply by adding function objects.
[Boost.Lambda2] is a Boost library (written by Peter) which makes it possible to write very terse, simple operations, by building upon the std::bind
machinery. When Barry was implementing std::views::zip
[P2321R2], a range adaptor whose implementation requires forwarding various operators across a tuple
, Boost.Lambda2 provided a very nice way to implement those operations. Here is a comparison between a hand-written lambda solution, function objects, and the placeholder solution that Lambda2 offers:
Handwritten Lambdas | Named Function Objects | Boost.Lambda2 |
---|---|---|
It’s not just that the Lambda2 alternatives are overwhelmingly terser (it’s very hard to beat 3 characters for the dereference operation, especially compared to the handwritten lambda that must use -> decltype(auto)
and is thus 46 characters long), they more directly express exactly the work being done.
Lambda2 also offers a more expressive way of doing common predicates, even in the case where the named function object already exists. Let’s take an example where you want to write a predicate for if the argument is negative (an example Barry previously wrote about on his blog here), there are several ways to do it:
It also allows for an approach to address the question of projections. Let’s say that rather than finding a negative number, we want to find a Point
whose x
coordinate is negative:
Or if the x
coordinate is 0:
Note that this latter usage could be improved significantly with something like [P0060R0], which would actually allow for writing the predicate _1.x == 0
. Which is difficult to beat.
You can see more examples in the [Boost.Lambda2] docs.
We propose to solve the issue of missing operator function objects in the standard library, as well as less-than-ergonomic lambda syntax for common predicates, by standardizing Boost.Lambda2. That is not a large proposal. The standard library already provides placeholders, std::placeholders::_1
and friends. The standard library also already provides std::bind
, which is already implemented in a way that supports composition of bind expressions. All we need to do is add operators.
We additionally add the missing operator function objects. Now, most of the missing operator function objects and placeholder operators are easy enough to add, except one: taking an object’s address.
&x
Now, this particular operator has two problems. First, making &_1
work requires overload unary operator&()
and that seems particularly questionable, even in cases like this. And in order to make this broadly useful, we couldn’t just overload it as a member function, it’d have to be a non-member - to support things like &*_1
or any other combination of operations (which is part of the value of Lambda2). That’s a bit too much code for having &x
not actually mean address-of.
We could potentially address this problem by adding in a function like std::placeholders::addr(x)
to mean addressof, so that instead of the cute &_1
syntax you’d have to write addr(_1)
, which doesn’t have any issues with &
. Note that we cannot call this function addressof
because while addressof(_1)
would be okay, addressof(addressof(_1))
would become ambiguous (unless we also change std::addressof
, as we’re about to discuss).
Second, the obvious name for a function object taking the address of an object would be std::addressof
- but that already exists, as a function template. We cannot change std::addressof
to be a type - that would break all code that uses it. We could potentially change it to be an object - that would break only ADL uses of it, but given the nature of std::addressof
those seem pretty unlikely to be common, so it’s potentially a feasible route to take. It would also allow _1->*std::addressof
(in the absence of addr(_1)
or similar formulation) as a short-ish way of expressing this.
For now, we’re going to punt on both problems and simply not support either a terse addressof on placeholders or providing an addressof function object.
Boost.Lambda2 additionally provides two helper function objects: first
and second
, such that _1->*first
gives you the first element of the type (as by std::get<0>
) and _1->*second
gives you the second. This is done by just providing function objects that perform these operations, similar to the proposed get_key
and get_value
[P2769R1].
Also, while most operators take forwarding references, there are two additional overloads of >>
and <<
which are special-cased such that operations like std::cout << _1
work and capture std::cout
by reference. The special-casing is necessary because otherwise std::cout
would be captured by value, which is not allowed, and users would have to write std::ref(std::cout) << _1
.
Has been shipping in Boost since 1.77 (August 2021).
Extend 22.10.2 [functional.syn] to add the additional function objects:
namespace std { // ... // [bitwise.operations], bitwise operations template<class T = void> struct bit_and; // freestanding template<class T = void> struct bit_or; // freestanding template<class T = void> struct bit_xor; // freestanding template<class T = void> struct bit_not; // freestanding template<> struct bit_and<void>; // freestanding template<> struct bit_or<void>; // freestanding template<> struct bit_xor<void>; // freestanding template<> struct bit_not<void>; // freestanding + // [additional.operations], additional transparent operations + struct subscript; // freestanding + struct left_shift; // freestanding + struct right_shift; // freestanding + struct unary_plus; // freestanding + struct dereference; // freestanding + struct increment; // freestanding + struct decrement; // freestanding + struct postfix_increment; // freestanding + struct postfix_decrement; // freestanding + + // [compound.operations], compound assignment operations + struct plus_equal; // freestanding + struct minus_equal; // freestanding + struct multiplies_equal; // freestanding + struct divides_equal; // freestanding + struct modulus_equal; // freestanding + struct bit_and_equal; // freestanding + struct bit_or_equal; // freestanding + struct bit_xor_equal; // freestanding + struct left_shift_equal; // freestanding + struct right_shift_equal; // freestanding // ... }
Extend 22.10.2 [functional.syn] to add operators:
namespace std { // ... namespace placeholders { // M is the implementation-defined number of placeholders see below _1; // freestanding see below _2; // freestanding . . . see below _M; // freestanding + template<class A, class B> constexpr auto operator+(A&&, B&&); // freestanding + template<class A, class B> constexpr auto operator-(A&&, B&&); // freestanding + template<class A, class B> constexpr auto operator*(A&&, B&&); // freestanding + template<class A, class B> constexpr auto operator/(A&&, B&&); // freestanding + template<class A, class B> constexpr auto operator%(A&&, B&&); // freestanding + template<class A> constexpr auto operator-(A&&); // freestanding + + template<class A, class B> constexpr auto operator==(A&&, B&&); // freestanding + template<class A, class B> constexpr auto operator!=(A&&, B&&); // freestanding + template<class A, class B> constexpr auto operator<(A&&, B&&); // freestanding + template<class A, class B> constexpr auto operator>(A&&, B&&); // freestanding + template<class A, class B> constexpr auto operator<=(A&&, B&&); // freestanding + template<class A, class B> constexpr auto operator>=(A&&, B&&); // freestanding + template<class A, class B> constexpr auto operator<=>(A&&, B&&); // freestanding + + template<class A, class B> constexpr auto operator&&(A&&, B&&); // freestanding + template<class A, class B> constexpr auto operator||(A&&, B&&); // freestanding + template<class A> constexpr auto operator!(A&&); // freestanding + + template<class A, class B> constexpr auto operator&(A&&, B&&); // freestanding + template<class A, class B> constexpr auto operator|(A&&, B&&); // freestanding + template<class A, class B> constexpr auto operator^(A&&, B&&); // freestanding + template<class A> constexpr auto operator~(A&&); // freestanding + + template<class A, class B> constexpr auto operator<<(A&&, B&&); // freestanding + template<class A, class B> constexpr auto operator<<(A&, B&&); // freestanding + + template<class A, class B> constexpr auto operator>>(A&&, B&&); // freestanding + template<class A, class B> constexpr auto operator>>(A&, B&&); // freestanding + + template<class A> constexpr auto operator+(A&&); // freestanding + template<class A> constexpr auto operator*(A&&); // freestanding + template<class A> constexpr auto operator++(A&&); // freestanding + template<class A> constexpr auto operator--(A&&); // freestanding + template<class A> constexpr auto operator++(A&&, int); // freestanding + template<class A> constexpr auto operator--(A&&, int); // freestanding + + template<class A, class B> constexpr auto operator+=(A&&, B&&); // freestanding + template<class A, class B> constexpr auto operator-=(A&&, B&&); // freestanding + template<class A, class B> constexpr auto operator*=(A&&, B&&); // freestanding + template<class A, class B> constexpr auto operator/=(A&&, B&&); // freestanding + template<class A, class B> constexpr auto operator%=(A&&, B&&); // freestanding + template<class A, class B> constexpr auto operator&=(A&&, B&&); // freestanding + template<class A, class B> constexpr auto operator|=(A&&, B&&); // freestanding + template<class A, class B> constexpr auto operator^=(A&&, B&&); // freestanding + template<class A, class B> constexpr auto operator<<=(A&&, B&&); // freestanding + template<class A, class B> constexpr auto operator>>=(A&&, B&&); // freestanding + + template<class A, class B> constexpr auto operator->*(A&&, B&&); // freestanding + + inline constexpr unspecified first = unspecified; // freestanding + inline constexpr unspecified second = unspecifeid; // freestanding } // ... }
Add two new sections after 22.10.11 [bitwise.operations]:
Additional operations [additional.operations]
Class
subscript
[additional.operations.subscript]struct subscript { template<class T, class U> constexpr auto operator()(T&& t, U&& u) const -> decltype(std::forward<T>(t)[std::forward<U>(u)]); using is_transparent = unspecified; };
template<class T, class U> constexpr auto operator()(T&& t, U&& u) const -> decltype(std::forward<T>(t)[std::forward<U>(u)]);
1 Returns:
std::forward<T>(t)[std::forward<U>(u)]
.Class
left_shift
[additional.operations.left_shift]struct left_shift { template<class T, class U> constexpr auto operator()(T&& t, U&& u) const -> decltype(std::forward<T>(t) << std::forward<U>(u)); using is_transparent = unspecified; };
template<class T, class U> constexpr auto operator()(T&& t, U&& u) const -> decltype(std::forward<T>(t) << std::forward<U>(u));
1 Returns:
std::forward<T>(t) << std::forward<U>(u)
.Class
right_shift
[additional.operations.right_shift]struct right_shift { template<class T, class U> constexpr auto operator()(T&& t, U&& u) const -> decltype(std::forward<T>(t) >> std::forward<U>(u)); using is_transparent = unspecified; };
template<class T, class U> constexpr auto operator()(T&& t, U&& u) const -> decltype(std::forward<T>(t) >> std::forward<U>(u));
1 Returns:
std::forward<T>(t) >> std::forward<U>(u)
.Class
unary_plus
[additional.operations.unary_plus]struct unary_plus { template<class T> constexpr auto operator()(T&& t) const -> decltype(+std::forward<T>(t)); using is_transparent = unspecified; };
1 Returns:
+std::forward<T>(t)
.Class
dereference
[additional.operations.dereference]struct dereference { template<class T> constexpr auto operator()(T&& t) const -> decltype(*std::forward<T>(t)); using is_transparent = unspecified; };
1 Returns:
*std::forward<T>(t)
.Class
increment
[additional.operations.increment]struct increment { template<class T> constexpr auto operator()(T&& t) const -> decltype(++std::forward<T>(t)); using is_transparent = unspecified; };
1 Returns:
++std::forward<T>(t)
.Class
decrement
[additional.operations.decrement]struct decrement { template<class T> constexpr auto operator()(T&& t) const -> decltype(--std::forward<T>(t)); using is_transparent = unspecified; };
1 Returns:
--std::forward<T>(t)
.Class
postfix_increment
[additional.operations.postfix_increment]struct postfix_increment { template<class T> constexpr auto operator()(T&& t) const -> decltype(std::forward<T>(t)++); using is_transparent = unspecified; };
1 Returns:
std::forward<T>(t)++
.Class
postfix_decrement
[additional.operations.postfix_decrement]struct postfix_decrement { template<class T> constexpr auto operator()(T&& t) const -> decltype(std::forward<T>(t)--); using is_transparent = unspecified; };
1 Returns:
std::forward<T>(t)--
.Compound assignment operations [compound.operations]
Class
plus_equal
[compound.operations.plus_equal]struct plus_equal { template<class T, class U> constexpr auto operator()(T&& t, U&& u) const -> decltype(std::forward<T>(t) += std::forward<U>(u)); using is_transparent = unspecified; };
template<class T, class U> constexpr auto operator()(T&& t, U&& u) const -> decltype(std::forward<T>(t) += std::forward<U>(u));
1 Returns:
std::forward<T>(t) += std::forward<U>(u)
.Class
minus_equal
[compound.operations.minus_equal]struct minus_equal { template<class T, class U> constexpr auto operator()(T&& t, U&& u) const -> decltype(std::forward<T>(t) -= std::forward<U>(u)); using is_transparent = unspecified; };
template<class T, class U> constexpr auto operator()(T&& t, U&& u) const -> decltype(std::forward<T>(t) -= std::forward<U>(u));
1 Returns:
std::forward<T>(t) -= std::forward<U>(u)
.Class
multiplies_equal
[compound.operations.multiplies_equal]struct multiplies_equal { template<class T, class U> constexpr auto operator()(T&& t, U&& u) const -> decltype(std::forward<T>(t) *= std::forward<U>(u)); using is_transparent = unspecified; };
template<class T, class U> constexpr auto operator()(T&& t, U&& u) const -> decltype(std::forward<T>(t) *= std::forward<U>(u));
1 Returns:
std::forward<T>(t) *= std::forward<U>(u)
.Class
divides_equal
[compound.operations.divides_equal]struct divides_equal { template<class T, class U> constexpr auto operator()(T&& t, U&& u) const -> decltype(std::forward<T>(t) /= std::forward<U>(u)); using is_transparent = unspecified; };
template<class T, class U> constexpr auto operator()(T&& t, U&& u) const -> decltype(std::forward<T>(t) /= std::forward<U>(u));
1 Returns:
std::forward<T>(t) /= std::forward<U>(u)
.Class
modulus_equal
[compound.operations.modulus_equal]struct modulus_equal { template<class T, class U> constexpr auto operator()(T&& t, U&& u) const -> decltype(std::forward<T>(t) %= std::forward<U>(u)); using is_transparent = unspecified; };
template<class T, class U> constexpr auto operator()(T&& t, U&& u) const -> decltype(std::forward<T>(t) %= std::forward<U>(u));
1 Returns:
std::forward<T>(t) %= std::forward<U>(u)
.Class
bit_and_equal
[compound.operations.bit_and_equal]struct bit_and_equal { template<class T, class U> constexpr auto operator()(T&& t, U&& u) const -> decltype(std::forward<T>(t) &= std::forward<U>(u)); using is_transparent = unspecified; };
template<class T, class U> constexpr auto operator()(T&& t, U&& u) const -> decltype(std::forward<T>(t) &= std::forward<U>(u));
1 Returns:
std::forward<T>(t) &= std::forward<U>(u)
.Class
bit_or_equal
[compound.operations.bit_or_equal]struct bit_or_equal { template<class T, class U> constexpr auto operator()(T&& t, U&& u) const -> decltype(std::forward<T>(t) |= std::forward<U>(u)); using is_transparent = unspecified; };
template<class T, class U> constexpr auto operator()(T&& t, U&& u) const -> decltype(std::forward<T>(t) |= std::forward<U>(u));
1 Returns:
std::forward<T>(t) |= std::forward<U>(u)
.Class
bit_xor_equal
[compound.operations.bit_xor_equal]struct bit_xor_equal { template<class T, class U> constexpr auto operator()(T&& t, U&& u) const -> decltype(std::forward<T>(t) ^= std::forward<U>(u)); using is_transparent = unspecified; };
template<class T, class U> constexpr auto operator()(T&& t, U&& u) const -> decltype(std::forward<T>(t) ^= std::forward<U>(u));
1 Returns:
std::forward<T>(t) ^= std::forward<U>(u)
.Class
left_shift_equal
[compound.operations.left_shift_equal]struct left_shift_equal { template<class T, class U> constexpr auto operator()(T&& t, U&& u) const -> decltype(std::forward<T>(t) <<= std::forward<U>(u)); using is_transparent = unspecified; };
template<class T, class U> constexpr auto operator()(T&& t, U&& u) const -> decltype(std::forward<T>(t) <<= std::forward<U>(u));
1 Returns:
std::forward<T>(t) <<= std::forward<U>(u)
.Class
right_shift_equal
[compound.operations.right_shift_equal]struct right_shift_equal { template<class T, class U> constexpr auto operator()(T&& t, U&& u) const -> decltype(std::forward<T>(t) >>= std::forward<U>(u)); using is_transparent = unspecified; };
template<class T, class U> constexpr auto operator()(T&& t, U&& u) const -> decltype(std::forward<T>(t) >>= std::forward<U>(u));
1 Returns:
std::forward<T>(t) >>= std::forward<U>(u)
.
Extend 22.10.15.5 [func.bind.place]:
namespace std::placeholders { // M is the number of placeholders + template <int J> + struct placeholder { // exposition only + template <class... Args> + constexpr decltype(auto) operator()(Args&&... ) const noexcept; + template <class T> + constexpr auto operator[](T&& ) const; + }; see below _1; see below _2; . . . see below _M; }
1 The number
M
of placeholders is implementation-defined.2 All placeholder types meet the Cpp17DefaultConstructible and Cpp17CopyConstructible requirements, and their default constructors and copy/move constructors are constexpr functions that do not throw exceptions. It is implementation-defined whether placeholder types meet the Cpp17CopyAssignable requirements, but if so, their copy assignment operators are constexpr functions that do not throw exceptions.
3 Placeholders should be defined as:
If they are not, they are declared as:
4 Placeholders are freestanding items ([freestanding.item]).
template <int J> template <class... Args> decltype(auto) placeholder<J>::operator()(Args&&... args) const noexcept;
5 Constraints:
sizeof...(Args) >= J
istrue
.6 Returns:
std::forward<Args>(args)...[J - 1]
.7 Returns:
bind(subscript(), *this, std::forward<T>(t))
.8 Each operator function declared in this clause is constrained on at least one of the parameters having a type
T
which satisfiesis_placeholder_v<remove_cvref_t<T>> || is_bind_expression_v<remove_cvref_t<T>>
istrue
.9 Returns:
bind(plus<>(), std::forward<A>(a), std::forward<B>(b))
.10 Returns:
bind(minus<>(), std::forward<A>(a), std::forward<B>(b))
.11 Returns:
bind(multiplies<>(), std::forward<A>(a), std::forward<B>(b))
.12 Returns:
bind(divides<>(), std::forward<A>(a), std::forward<B>(b))
.13 Returns:
bind(modulus<>(), std::forward<A>(a), std::forward<B>(b))
.14 Returns:
bind(negate<>(), std::forward<A>(a))
.15 Returns:
bind(equal_to<>(), std::forward<A>(a), std::forward<B>(b))
.16 Returns:
bind(not_equal_to<>(), std::forward<A>(a), std::forward<B>(b))
.17 Returns:
bind(less<>(), std::forward<A>(a), std::forward<B>(b))
.18 Returns:
bind(greater<>(), std::forward<A>(a), std::forward<B>(b))
.19 Returns:
bind(less_equal<>(), std::forward<A>(a), std::forward<B>(b))
.20 Returns:
bind(greater_equal<>(), std::forward<A>(a), std::forward<B>(b))
.21 Returns:
bind(compare_three_way(), std::forward<A>(a), std::forward<B>(b))
.22 Returns:
bind(logical_and<>(), std::forward<A>(a), std::forward<B>(b))
.23 Returns:
bind(logical_or<>(), std::forward<A>(a), std::forward<B>(b))
.24 Returns:
bind(logical_not<>(), std::forward<A>(a))
.25 Returns:
bind(bit_and<>(), std::forward<A>(a), std::forward<B>(b))
.26 Returns:
bind(bit_or<>(), std::forward<A>(a), std::forward<B>(b))
.27 Returns:
bind(bit_xor<>(), std::forward<A>(a), std::forward<B>(b))
.28 Returns:
bind(bit_not<>(), std::forward<A>(a))
.29 Constraints:
is_base_of_v<ios_base, remove_cvref_t<A>>
isfalse
.30 Returns:
bind(left_shift(), std::forward<A>(a), std::forward<B>(b))
.31 Constraints:
is_base_of_v<ios_base, remove_cvref_t<A>>
istrue
.32 Returns:
bind(left_shift(), ref(a), std::forward<B>(b))
.33 Remarks: This overload allows expressions like
std::cout << _1 << '\n'
to work.34 Constraints:
is_base_of_v<ios_base, remove_cvref_t<A>>
isfalse
.35 Returns:
bind(right_shift(), std::forward<A>(a), std::forward<B>(b))
.36 Constraints:
is_base_of_v<ios_base, remove_cvref_t<A>>
istrue
.37 Returns:
bind(right_shift(), ref(a), std::forward<B>(b))
.38 Returns:
bind(unary_plus(), std::forward<A>(a))
.39 Returns:
bind(dereference(), std::forward<A>(a))
.40 Returns:
bind(increment(), std::forward<A>(a))
.41 Returns:
bind(decrement(), std::forward<A>(a))
.42 Returns:
bind(postfix_increment(), std::forward<A>(a))
.43 Returns:
bind(postfix_decrement(), std::forward<A>(a))
.44 Returns:
bind(plus_equal(), std::forward<A>(a), std::forward<B>(b))
.45 Returns:
bind(minus_equal(), std::forward<A>(a), std::forward<B>(b))
.46 Returns:
bind(multiplies_equal(), std::forward<A>(a), std::forward<B>(b))
.47 Returns:
bind(divides_equal(), std::forward<A>(a), std::forward<B>(b))
.48 Returns:
bind(modulus_equal(), std::forward<A>(a), std::forward<B>(b))
.49 Returns:
bind(bit_and_equal(), std::forward<A>(a), std::forward<B>(b))
.50 Returns:
bind(bit_or_equal(), std::forward<A>(a), std::forward<B>(b))
.51 Returns:
bind(bit_xor_equal(), std::forward<A>(a), std::forward<B>(b))
.52 Returns:
bind(left_shift_equal(), std::forward<A>(a), std::forward<B>(b))
.53 Returns:
bind(right_shift_equal(), std::forward<A>(a), std::forward<B>(b))
.54 Returns:
bind(std::forward<B>(b), std::forward<A>(a))
.55 The name
first
denotes a customization point object ([customization.point.object]). Given a subexpressionE
:
- (55.1) If
E
has class or enumeration type andget<0>(E)
is a valid expression where the meaning ofget
is established by performing argument-dependent lookup only ([basic.lookup.argdep]), thenfirst(E)
is expression-equivalent toget<0>(E)
.- (55.2) Otherwise,
first(E)
is ill-formed.56 The name
second
denotes a customization point object ([customization.point.object]). Given a subexpressionE
:
Add an entry to 17.3.2 [version.syn] for this
[Boost.Lambda2] Peter Dimov. 2020. Lambda2: A C++14 Lambda Library.
https://www.boost.org/doc/libs/master/libs/lambda2/doc/html/lambda2.html
[P0060R0] Mathias Gaunard, Dietmar Kühl. 2015-09-18. Function Object-Based Overloading of Operator Dot.
https://wg21.link/p0060r0
[P2321R2] Tim Song. 2021-06-11. zip.
https://wg21.link/p2321r2
[P2760R0] Barry Revzin. 2023-09-17. A Plan for C++26 Ranges.
https://wg21.link/p2760r0
[P2769R1] Ruslan Arutyunyan, Alexey Kukanov. 2023-05-17. get_element customization point object.
https://wg21.link/p2769r1