Add identity element interface sketch and use cases
Add use cases showing sketches of an interface for specifying an identity element.
Interface sketch
Users specify identity element with a named struct reduce_identity. Its default specialization looks like this.
template<class T>
struct reduce_identity {
T value{};
};
The default identity is T{}. Users have two ways to use a nondefault value.
- Specify the value:
reduce_identity{value}
- Specialize
reduce_identity<T>
Option (1) supports the use case where the same reduction result type might be used with different binary operators. For example, users might have some parts of their code where they use double values with the normal floating-point algebra, and other parts where they use double values with the min-plus ("tropical") algebra. For the former case, the default reduce_identity<double> would suffice. For the latter case, users would specify the identity as reduce_identity{numeric_limits<double>::infinity()}.
Option (2) lets users specify the value at compile time if they want.
namespace impl {
inline constexpr T some_value = /* value goes here */;
}
template<class T>
struct reduce_identity<T> : constant_wrapper<impl::some_value> {};
Should the default identity value depend on the binary operation?
It would be more user-friendly if the default identity value could depend on the binary operation. For example, the default identity value for std::multiplies<float>{} should be 1.0f. This would imitate OpenACC and OpenMP behavior.
One way to achieve this goal would be to have special cases for known combinations of binary operator type and reduction result type. For example, the identity for std::multiplies<float> would be 1.0f. The problem with this approach is that users often use the Standard Library binary operator types with their default template argument void. std::multiples<void> just means "use binary operator*." This would give incorrect results for user-defined reduction result types. For example, if users define a min-plus algebra number type MinPlus with underlying type float, its overloaded operator* would actually do addition on float values, and thus should have identity value 0.0f.
Another way to achieve this goal would be to template reduce_identity on the binary operation type BinaryOp, instead of (or in addition to) the reduction result type T. However, this would have at least two issues.
- Users would not be able to specialize
reduce_identity for lambdas, since every lambda declaration declares a unique type.
- Binary operations with the same return type can themselves have different types.
Here is an example of (2).
float binary_op_1(const float& x, const float& y) {
return x + y;
}
float binary_op_2(float x, float y) {
return x + y;
}
static_assert(! std::is_same_v<
decltype(binary_op_1),
decltype(binary_op_2)>);
static_assert(std::is_invocable_r_v<float,
decltype(binary_op_1), float, float>);
static_assert(std::is_invocable_r_v<float,
decltype(binary_op_2), float, float>);
A result is that users might try to specialize on a function float(float, float), but then define the actual function as float(const float&, const float&). This would give them the wrong specialization of reduce_identity at the point of use without a compile-time error.
Users might try to solve this by writing a partial specialization of reduce_identity constrained on "any binary function invocable with two arguments U and V and returning T." However, correctly constraining U and V is difficult (range value type, reduction result type, or some intermediate type?). More importantly, the partial specialization would take effect for all binary operations on T, not just for the user's binary operation.
We conclude that it's not worth trying to make reduce_identity depend at compile time on the binary operation. The following decision tree explains how users can specify the identity value.
-
If T{} is the identity for the binary operation, rely on default reduce_identity<T>{}.value;
-
otherwise, if T{} is NOT the identity for the binary operation:
a. If the binary operation is always T operator+(x,y) or std::plus<T>:
i. specialize `reduce_identity<T>`, or
ii. provide a value `reduce_identity<T>{value}` at the point of use;
b. otherwise, provide a value reduce_identity<T>{value} at the point of use.
If users can specify the identity value, should they also specify an initial value?
The initial value parameter T init of C++17 std::reduce has two purposes.
- It specifies the actual initial value, which lets users compute partial reductions.
- It determines
std::reduce's return type.
If users could specify the identity value, that would also determine std::reduce's result type. In fact, it would do so redundantly, because for C++17 std::reduce, the initial value type is the return type.
The ranges::fold_left algorithm does not need an identity value, for reasons we explain elsewhere in this proposal. However, it requires a (nonoptional) initial value. The initial value of ranges::fold_left determines the return type of ranges::fold_left, but is not necessarily the same as the return type. The actual return type depends on the binary operation, the range value type, and the initial value type.
It's an important feature of both ranges::fold_left and of our ranges::reduce that we propose here, to be able to deduce a default initial value type from the range type.
What should happen if users could specify both an initial value and an identity value, and those types differ? Should that be ill-formed?
Use cases
Ordinary floating-point plus, range of double.
auto id = reduce_identity<double>{};
auto result1 = ranges::reduce(exec_policy, range, plus{}, id);
// Default identity value is reduce_identity<double>{}
auto result2 = ranges::reduce(exec_policy, range, plus{});
Min reduction (e.g., for min-plus (tropical) algebra) on double.
auto binary_op = [] (double y) { return std::fmin(x, y); }
auto id = reduce_identity{numeric_limits<double>::infinity()}
// No initial value, just identity value
ranges::reduce(exec_policy, range, binary_op, id);
Add identity element interface sketch and use cases
Add use cases showing sketches of an interface for specifying an identity element.
Interface sketch
Users specify identity element with a named struct
reduce_identity. Its default specialization looks like this.The default identity is
T{}. Users have two ways to use a nondefault value.reduce_identity{value}reduce_identity<T>Option (1) supports the use case where the same reduction result type might be used with different binary operators. For example, users might have some parts of their code where they use
doublevalues with the normal floating-point algebra, and other parts where they usedoublevalues with the min-plus ("tropical") algebra. For the former case, the defaultreduce_identity<double>would suffice. For the latter case, users would specify the identity asreduce_identity{numeric_limits<double>::infinity()}.Option (2) lets users specify the value at compile time if they want.
Should the default identity value depend on the binary operation?
It would be more user-friendly if the default identity value could depend on the binary operation. For example, the default identity value for
std::multiplies<float>{}should be1.0f. This would imitate OpenACC and OpenMP behavior.One way to achieve this goal would be to have special cases for known combinations of binary operator type and reduction result type. For example, the identity for
std::multiplies<float>would be1.0f. The problem with this approach is that users often use the Standard Library binary operator types with their default template argumentvoid.std::multiples<void>just means "use binaryoperator*." This would give incorrect results for user-defined reduction result types. For example, if users define a min-plus algebra number typeMinPluswith underlying typefloat, its overloadedoperator*would actually do addition onfloatvalues, and thus should have identity value0.0f.Another way to achieve this goal would be to template
reduce_identityon the binary operation typeBinaryOp, instead of (or in addition to) the reduction result typeT. However, this would have at least two issues.reduce_identityfor lambdas, since every lambda declaration declares a unique type.Here is an example of (2).
A result is that users might try to specialize on a function
float(float, float), but then define the actual function asfloat(const float&, const float&). This would give them the wrong specialization ofreduce_identityat the point of use without a compile-time error.Users might try to solve this by writing a partial specialization of
reduce_identityconstrained on "any binary function invocable with two argumentsUandVand returningT." However, correctly constrainingUandVis difficult (range value type, reduction result type, or some intermediate type?). More importantly, the partial specialization would take effect for all binary operations onT, not just for the user's binary operation.We conclude that it's not worth trying to make
reduce_identitydepend at compile time on the binary operation. The following decision tree explains how users can specify the identity value.If
T{}is the identity for the binary operation, rely on defaultreduce_identity<T>{}.value;otherwise, if
T{}is NOT the identity for the binary operation:a. If the binary operation is always
T operator+(x,y)orstd::plus<T>:b. otherwise, provide a value
reduce_identity<T>{value}at the point of use.If users can specify the identity value, should they also specify an initial value?
The initial value parameter
T initof C++17std::reducehas two purposes.std::reduce's return type.If users could specify the identity value, that would also determine
std::reduce's result type. In fact, it would do so redundantly, because for C++17std::reduce, the initial value type is the return type.The
ranges::fold_leftalgorithm does not need an identity value, for reasons we explain elsewhere in this proposal. However, it requires a (nonoptional) initial value. The initial value ofranges::fold_leftdetermines the return type ofranges::fold_left, but is not necessarily the same as the return type. The actual return type depends on the binary operation, the range value type, and the initial value type.It's an important feature of both
ranges::fold_leftand of ourranges::reducethat we propose here, to be able to deduce a default initial value type from the range type.What should happen if users could specify both an initial value and an identity value, and those types differ? Should that be ill-formed?
Use cases
Ordinary floating-point plus, range of
double.Min reduction (e.g., for min-plus (tropical) algebra) on
double.