-
Notifications
You must be signed in to change notification settings - Fork 116
[oneTBB] Improve named requirements to better deal with value categories #647
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 3 commits
162fed2
b2431ce
2c5ebc0
f0cc309
68826e1
c47c112
a940d5c
b724eb5
17fdde7
d549142
b13abab
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -22,9 +22,14 @@ an array to be sorted. A type ``T`` would be *sortable* if: | |
|
||
You can write a sorting template function in C++ that sorts an array of any type that is *sortable*. | ||
|
||
.. _pseudo_signatures: | ||
|
||
Pseudo-Signatures | ||
----------------- | ||
|
||
Two approaches for defining named requirements are *valid expressions* and *pseudo-signatures*. | ||
The ISO C++ standard follows the valid *expressions* approach, which shows what the usage pattern looks like for a requirement. | ||
It has the drawback of relegating important details to notational conventions. This document uses | ||
It has the drawback of relegating important details to notational conventions. This document mostly uses | ||
pseudo-signatures because they are concise and can be cut-and-pasted for an initial implementation. | ||
|
||
For example, the table below shows pseudo-signatures for a *sortable* type ``T``: | ||
|
@@ -43,12 +48,18 @@ For example, the table below shows pseudo-signatures for a *sortable* type ``T`` | |
|
||
--------------------------------------------------------------------------------------------- | ||
|
||
A pseudo-signature describes how an implementation interacts with a type or a function. | ||
A real signature may differ from the pseudo-signature that it implements in ways where implicit | ||
akukanov marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
conversions would deal with the difference. For an example type ``U``, the real signature that | ||
implements ``operator<`` in the table above can be expressed as ``int operator<( U x, U y )``, | ||
because C++ permits implicit conversion from ``int`` to ``bool``, and implicit conversion from ``U`` | ||
to (``const U&``). Similarly, the real signature ``bool operator<( U& x, U& y )`` is acceptable | ||
because C++ permits implicit addition of a const qualifier to a reference type. | ||
conversions would deal with the difference: function parameters need to implicitly convert | ||
from the ones in the pseudo-signature, and return value needs to implicitly convert to the one | ||
akukanov marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
in the pseudo-signature. | ||
|
||
For an example type ``U``, the real signature that implements ``operator<`` in the table above | ||
can be expressed as ``int operator<( U x, U y )``, because C++ permits implicit conversion from | ||
``int`` to ``bool``, and implicit conversion from ``const U&`` to ``U`` if the type is copyable. | ||
akukanov marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
For a counter-example, the real signature ``bool operator<( U& x, U& y )`` is not acceptable | ||
because C++ does not permit implicit removal of a const qualifier from a type, and so the code | ||
akukanov marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
would not compile if the implementation attempts to pass a const object to the function. | ||
|
||
|
||
Algorithms | ||
---------- | ||
|
@@ -81,7 +92,6 @@ Mutexes | |
|
||
Containers | ||
---------- | ||
|
||
.. toctree:: | ||
:titlesonly: | ||
|
||
|
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
|
@@ -15,46 +15,28 @@ It should also meet one of the following requirements: | |||||
|
||||||
**ParallelForEachBody Requirements: Pseudo-Signature, Semantics** | ||||||
|
||||||
.. cpp:function:: Body::operator()( ItemType item ) const | ||||||
.. cpp:function:: void Body::operator()( ReferenceType item ) const | ||||||
|
||||||
Process the received item. | ||||||
|
||||||
.. cpp:function:: Body::operator()( ItemType item, oneapi::tbb::feeder<ItemType>& feeder ) const | ||||||
.. cpp:function:: void Body::operator()( ItemType&& item, oneapi::tbb::feeder<ItemType>& feeder ) const | ||||||
|
.. cpp:function:: void Body::operator()( ItemType&& item, oneapi::tbb::feeder<ItemType>& feeder ) const | |
.. cpp:function:: void Body::operator()( ReferenceType item, oneapi::tbb::feeder<ItemType>& feeder ) const |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This question is related to the question below about "removing" a requirement:
Additional elements submitted into
oneapi::tbb::parallel_for_each
through thefeeder::add
are passed to theBody
as rvalues. In this case, the corresponding execution of theBody
is required to be well-formed.
As I understand it, it says that if the feeder is used, the body has to accept rvalues - because the work elements supplied through the feeder will be "moved" to the body for processing. Since parallel_for_each
may only use one operator()
, I interpret the requirement as "if your body operator takes a feeder, it has to accept rvalues" - and that is exactly what the pseudo-signature describes.
If the idea is that the original sequence elements are passed just as the result of iterator dereferencing (i.e., *it
) while new work elements are "moved" - the operator should then support both ReferenceType
(which might be an lvalue reference) and ItemType&&
rvalue reference - so it seemingly should take arguments either by value or by const reference. Neither allows modification of the original sequence (which can be fine, given the use of feeder); taking by value will have some copy construction overhead, and taking by const reference makes move useless.
Another option is to use a templated operator with universal references or use different overloads for lvalues and rvalues.
Let's figure out what semantics we want, how it could be implemented in practice, and then hopefully it will be more clear how to describe that in the named requirement.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, the original idea was that the elements of the input sequence are passed as the result of dereferencing. and additional items are always provided as rvalues.
The semantics proposed would break the first part, for example if the iterator have non-const lvalue reference
, *it
will not be accepted by the body. And the opposite is true, if the operator accepts non-const lvalue reference argument, passing of new items through feeder would not work.
I disagree that parallel_for_each
may use only one operator()
. I think it should be possible to have several overloads with different first argument types for input sequence and feeder items. By the way, current implementation of oneTBB would also work if the input sequence dereference is passed to the overload with feeder and the additional items are passed to the overload without the feeder:
struct body {
// overload for feeder items
void operator()(ItemType&& value_from_feeder) const {}
// overload for input sequence
void operator()(ItemType& input_sequence, oneapi::tbb::feeder<ItemType>& feeder) const {}
};
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I disagree that parallel_for_each may use only one operator(). I think it should be possible to have several overloads with different first argument types for input sequence and feeder items.
I meant. only one of the two specified operators - either with or without the feeder, but not both. Several overloads with the feeder would be fine, if there is a good rationale for that.
By the way, current implementation of oneTBB would also work if the input sequence dereference is passed to the overload with feeder and the additional items are passed to the overload without the feeder:
This is simply an incorrect behavior. The choice of using or not using the feeder should not depend on how an item was obtained or on whether we can move it or not. Yes, we can distinguish between these sources of input, but why do we think it matters for the user, based only on the type of the parameter? After all, the feeder can be ignored when provided, but it cannot be created when absent,
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The eventual conclusion is that the first parameter should accept both ReferenceType
(for values from the input sequence of parallel_for_each
) and ItemType&&
(for values passed through feeder
). It is likely best described by two separate pseudo-signatures, though in some cases a single function can implement both (e.g. with the const ItemType&
parameter).
Outdated
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This document specify that the body should meet one of the requirements - either be invocable without a feeder or with it.
Should it be specified/recommended which overload should be preferred if both of them are present.
oneTBB implementation prefers the overload with the feeder.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, I think it should be specified. I do not see how the implementation could use both overloads; it would need to decide whether to provide the feeder or not, and how would it decide? So using the overload with the feeder all the time is the "safest" choice.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For now, I added a note/requirement that exactly one kind of operator()
should be provided. It is discussable though.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is it intentional that this requirement is removed?
Uh oh!
There was an error while loading. Please reload this page.