-
Notifications
You must be signed in to change notification settings - Fork 1.2k
Add RFC for tbb::optional #2040
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
Open
kboyarinov
wants to merge
1
commit into
master
Choose a base branch
from
dev/kboyarinov/rfc-tbb-optional
base: master
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,392 @@ | ||
| # ``tbb::optional`` and **Optional-Like** Named Requirement | ||
|
|
||
| ## Table of Contents | ||
|
|
||
| * 1 [Motivation](#motivation) | ||
| * 1.1 [The `tbb::flow_control` Problem](#the-tbbflow_control-problem) | ||
| * 1.2 [Default Constructible Requirements on Flow Graph Node Inputs](#default-constructible-requirements-on-flow-graph-node-inputs) | ||
| * 1.3 [Message Filtering for Flow Graph Nodes](#message-filtering-for-flow-graph-nodes) | ||
| * 2 [Concrete Proposal](#concrete-proposal) | ||
| * 2.1 [`tbb::optional` API](#tbboptional-api) | ||
| * 2.1.1 [Functionality of C++17 `std::optional` Excluded from `tbb::optional`](#functionality-of-c17-stdoptional-excluded-from-tbboptional) | ||
| * 2.1.2 [C++17 Behavior](#c17-behavior) | ||
| * 2.2 [**Optional-Like** Named Requirement](#optional-like-named-requirement) | ||
| * 2.3 [`tbb::flow_control` deprecation](#tbbflow_control-deprecation) | ||
| * 3 [Possible Additional Applications to Consider](#possible-additional-applications-to-consider) | ||
| * 4 [Open Questions](#open-questions) | ||
|
|
||
| ## Motivation | ||
|
|
||
| ``std::optional`` is one of the most impactful Standard Library types introduced in modern C++. It represents a | ||
| fundamental concept of a "value that might not be present" and is widely used to replace ad-hoc conventions like | ||
| sentinel values, using ``std::pair<T, bool>`` as a return type and others. | ||
|
|
||
| oneTBB supports C++11 as a minimum C++ Standard version, which means ``std::optional`` is not directly available | ||
| across the entire support matrix. This forces the library to implement internal workarounds where optional-like | ||
| semantics are required. | ||
|
|
||
| TBB already provides a custom union-based optional internally for the resource-limiting protocol used by | ||
| ``tbb::flow::resource_limited_node`` and ``tbb::flow::resource_limiter``. | ||
|
|
||
| ### The ``tbb::flow_control`` Problem | ||
|
|
||
| Currently, ``tbb::flow::input_node`` and ``tbb::parallel_pipeline`` first-filter bodies use ``tbb::flow_control`` | ||
| as a signaling mechanism to indicate end-of-input: | ||
|
|
||
| ```cpp | ||
| // body of tbb::flow::input_node<int> | ||
| int operator()(tbb::flow_control& fc) { | ||
| if (end-of-input) { | ||
| fc.stop(); | ||
| return 0; // dummy value | ||
| } | ||
| return next_input; | ||
| } | ||
| ``` | ||
|
|
||
| This API requires the user to construct a throw-away value when signaling stop. TBB runtime guarantees | ||
| to discard it if ``flow_control`` was stopped, but a valid output must still be provided, which is | ||
| unintuitive and error-prone. | ||
|
|
||
| In some cases, such an API implicitly requires the output type to be default constructible (although not | ||
| formally required) since a default constructed object is the intuitive choice for signaling end-of-input when | ||
| no sentinel is available. Non-default-constructible output types require contrived workarounds. | ||
|
|
||
| With ``std::optional``, this same body becomes self-documenting: | ||
|
|
||
| ```cpp | ||
| std::optional<int> operator()() { | ||
| if (end-of-input) { | ||
| return {}; | ||
| } | ||
| return next_input; | ||
| } | ||
| ``` | ||
|
|
||
| ### Default Constructible Requirements on Flow Graph Node Inputs | ||
|
|
||
| Several Flow Graph nodes impose ``DefaultConstructible`` requirements on inputs or outputs even though neither | ||
| the user's body nor the node itself logically needs default-constructed objects: | ||
|
|
||
| * ``function_node`` with ``rejecting`` policy that uses default-constructed ``input_type`` | ||
| as a placeholder before getting items from predecessors. | ||
| * ``multifunction_node`` with ``rejecting`` policy (same pattern as for ``function_node``). | ||
| * ``async_node`` which inherits the limitation from ``multifunction_node``. | ||
| * ``input_node`` initially default-constructs the cached item. | ||
| * ``overwrite_node`` initially default-constructs the buffered item. | ||
| * ``write_once_node`` which inherits the limitation from ``overwrite_node``. | ||
| * ``join_node`` initially default-constructs the output tuple, then fills it element-by-element. | ||
| * ``limiter_node`` that default-constructs the value before trying to reserve the item. | ||
|
|
||
| Using ``std::optional`` internally for cached/buffered items can eliminate these requirements, | ||
| allowing non-default-constructible types as messages. | ||
|
|
||
| ### Message Filtering for Flow Graph Nodes | ||
|
|
||
| We also have a long-standing idea to provide filtering support for Flow Graph functional | ||
| nodes (e.g., ``function_node``) to avoid broadcasting the message to successors if some conditions are satisfied. | ||
|
|
||
| Currently, the only way to implement this is to use a ``multifunction_node`` and conditionally put items to an output port: | ||
|
|
||
| ```cpp | ||
| multifunction_node<int, std::tuple<int>> | ||
| filter(g, unlimited, [](int input, auto& output_ports) { | ||
| int output = get_output(input); | ||
| if (should-broadcast) { | ||
| std::get<0>(output_ports).try_put(output); | ||
| } | ||
| }); | ||
| ``` | ||
|
|
||
| An alternative could be returning ``std::optional<Output>`` from ``function_node``'s body. | ||
| If the optional object is empty, the item is not broadcast: | ||
|
|
||
| ```cpp | ||
| function_node<int, int> | ||
| filter(g, unlimited, [](int input) -> std::optional<int> { | ||
| int output = get_output(input); | ||
| if (should-broadcast) { | ||
| return {output}; | ||
| } else { | ||
| return {}; | ||
| } | ||
| }); | ||
| ``` | ||
|
|
||
| ## Concrete Proposal | ||
|
|
||
| Since ``std::optional`` is only available starting from C++17, and oneTBB supports C++11 as the minimum standard, | ||
| an alternative type should be provided for C++11/C++14. | ||
|
|
||
| The proposal is | ||
| * Extend the current union-based optional implementation used by resource-limited Flow Graph nodes to include | ||
| a core subset of ``std::optional`` operations. Make this implementation available as the public ``tbb::optional``. | ||
| * Introduce the special named requirement **Optional-Like** to allow using ``tbb::optional``, ``std::optional`` | ||
| or a minimal user class in ``parallel_pipeline``, ``input_node`` and in Flow Graph message filters in the future. | ||
| * Deprecate ``tbb::flow_control``. | ||
| * Relax named requirements for types where possible. | ||
|
|
||
| ### ``tbb::optional`` API | ||
|
|
||
| ``tbb::optional`` API should implement a core subset of ``std::optional`` operations which are sufficient for C++11/C++14. | ||
|
|
||
| ```cpp | ||
| // Defined in header <oneapi/tbb/optional.h> | ||
|
|
||
| namespace oneapi { | ||
| namespace tbb { | ||
|
|
||
| struct in_place_t; | ||
|
|
||
| template <typename T> | ||
| class optional { | ||
| public: | ||
| using value_type = T; | ||
|
|
||
| optional() noexcept; | ||
| optional(const optional& other); | ||
| optional(optional&& other) noexcept(std::is_nothrow_move_constructible<T>::value); | ||
|
|
||
| template <typename... Args> | ||
| optional(in_place_t, Args&&... args); | ||
|
|
||
| ~optional(); | ||
|
|
||
| optional& operator=(const optional& other); | ||
| optional& operator=(optional&& other) | ||
| noexcept(std::is_nothrow_move_constructible<T>::value && | ||
| std::is_nothrow_move_assignable<T>::value); | ||
|
|
||
| const T* operator->() const noexcept; | ||
| T* operator->() noexcept; | ||
|
|
||
| const T& operator*() const noexcept; | ||
| T& operator*() noexcept; | ||
|
|
||
| explicit operator bool() const noexcept; | ||
| bool has_value() const noexcept; | ||
|
|
||
| const T& value() const; | ||
| T& value(); | ||
|
|
||
| template <typename... Args> | ||
| T& emplace(Args&&... args); | ||
|
|
||
| void swap(optional& other) | ||
| noexcept(std::is_nothrow_move_constructible<T>::value && | ||
| is-nothrow-swappable<T>::value); // Equivalent to C++17 std::is_nothrow_swappable | ||
|
|
||
| void reset() noexcept; | ||
| }; | ||
|
|
||
| template <typename T, typename... Args> | ||
| optional<T> make_optional(Args&&... args); | ||
|
|
||
| template <typename T> | ||
| void swap(optional<T>& lhs, optional<T>& rhs) noexcept(noexcept(lhs.swap(rhs))); | ||
|
|
||
| } // namespace tbb | ||
| } // namespace oneapi | ||
| ``` | ||
|
|
||
| #### Functionality of C++17 ``std::optional`` Excluded from ``tbb::optional`` | ||
|
|
||
| The following functionality is proposed to be excluded from the set of core operations: | ||
|
|
||
| ```cpp | ||
| // Default-constructed optional can be used instead | ||
| struct nullopt_t; | ||
|
|
||
| // Inline variables are not available in C++11 | ||
| inline constexpr nullopt_t nullopt; | ||
| inline constexpr in_place_t in_place; | ||
|
|
||
| template <typename T> | ||
| class optional { | ||
| // Equivalent to default-constructed optional | ||
| optional() noexcept; | ||
|
|
||
| // In-place construction can be used instead | ||
| template <typename U> | ||
| optional(const optional<U>& other); | ||
|
|
||
| template <typename U> | ||
| optional(optional<U>&& other); | ||
|
|
||
| template <typename U, typename... Args> | ||
| optional(in_place_t, std::initializer_list<U>, Args&&... args); | ||
|
|
||
| template <typename U = std::remove_cv_t<T>> | ||
| optional(U&& value); | ||
|
|
||
| // Equivalent to opt = tbb::optional{};, or reset | ||
| optional& operator=(nullopt_t) noexcept; | ||
|
|
||
| // Explicit support for lvalue and rvalue optionals | ||
| // Easy-to-implement, can be included in the core functionality if needed | ||
| T& value() &; | ||
| const T& value() const &; | ||
|
|
||
| T&& value() &&; | ||
| const T&& value() const &&; | ||
|
|
||
| // Can be easily rewritten using has_value | ||
| template <typename U = std::remove_cv<T>> | ||
| T value_or(U&& default_value) const &; | ||
|
|
||
| template <typename U = std::remove_cv<T>> | ||
| T value_or(U&& default_value) &&; | ||
|
|
||
| template <typename U, typename... Args> | ||
| T& emplace(std::initializer_list<U>, Args&&...); | ||
| }; | ||
|
|
||
| // In-place make_optional can be used instead | ||
| template <typename T> | ||
| optional<std::decay<T>> make_optional(T&& value); | ||
|
|
||
| template <typename T, typename U, typename... Args> | ||
| optional<T> make_optional(std::initializer_list<U> il, Args&&... args); | ||
|
|
||
| // Accessing an empty optional can be documented as UB | ||
| struct bad_optional_access; | ||
|
|
||
| // Value comparisons can be used directly | ||
| template <typename T, typename U> | ||
| bool operator==(const optional<T>& lhs, const optional<U>& rhs); | ||
| // also operators !=, <, >, <=, >= | ||
|
|
||
| template <typename T> | ||
| bool operator==(const optional<T>& lhs, nullopt_t); | ||
|
|
||
| template <typename T> | ||
| bool operator==(nullopt_t, const optional<T>& rhs); | ||
| // also operators !=, <, >, <=, >= | ||
|
|
||
| template <typename T, typename U> | ||
| bool operator==(const optional<T>& lhs, const U& rhs); | ||
|
|
||
| template <typename T, typename U> | ||
| bool operator==(const U& lhs, const optional<U>& rhs); | ||
| // also operators !=, <, >, <=, >= | ||
| ``` | ||
|
|
||
| Additionally, it is proposed to drop some constraints and requirements used by ``std::optional``. | ||
| For example, ``std::optional<T>`` copy constructor is defined as deleted if ``T`` is not copy constructible. | ||
| For ``tbb::optional``, it is proposed to document this as undefined behavior. | ||
|
|
||
| Any of the functions and restrictions listed above can be easily included into the core | ||
| functionality of ``tbb::optional`` in the future upon request. | ||
|
|
||
| #### C++17 Behavior | ||
|
|
||
| There are three options for ``tbb::optional`` in C++17 mode (where ``__cpp_lib_optional`` is defined): | ||
| 1. ``tbb::optional`` can be completely removed. | ||
| 2. ``tbb::optional`` can be an alias to ``std::optional``. | ||
| 3. ``tbb::optional`` can be kept as-is. | ||
|
|
||
| The issue with option 1 is that the users who migrate from C++11/14 to C++17 would need to rewrite | ||
| the code to use ``std::optional``. | ||
|
|
||
| For option 2, such a migration does not require changing the code, but C++11/14 ``tbb::optional`` | ||
| becomes binary incompatible with C++17 ``tbb::optional``. | ||
|
|
||
| Option 3 preserves both support for old code and binary compatibility for ``tbb::optional`` and therefore | ||
| is currently proposed. Additionally, implicit converting constructors and assignment operators from/to | ||
| ``std::optional`` can be defined in ``tbb::optional``. | ||
|
|
||
| ### **Optional-Like** Named Requirement | ||
|
|
||
| An **Optional-Like** named requirement should be defined in TBB to allow ``input_node`` body and ``parallel_pipeline`` | ||
| first-filter body to use both ``tbb::optional`` and ``std::optional``. | ||
|
|
||
| The type ``T`` satisfies the requirements of **Optional-Like** for value type ``U`` when the following | ||
| operations are supported: | ||
|
|
||
| Let ``t`` be an object of ``T`` and ``ct`` be an object of ``const T``. | ||
|
|
||
| | Operation | Precondition | Return Type | Name | | ||
| |-----------------------------|---------------------|--------------|----------------------------------| | ||
| | ``bool(ct)`` | N/A | ``bool`` | Conversion to ``bool`` | | ||
| | ``*t`` | ``bool(t) == true`` | ``U&`` | Access the value | | ||
| | ``*ct`` | ``bool(ct) == true``| ``const U&`` | Access the value | | ||
|
|
||
| These named requirements are satisfied by ``tbb::optional``, ``std::optional``, ``boost::optional`` or | ||
| any user-defined lightweight optional. | ||
|
|
||
| ### ``tbb::flow_control`` deprecation | ||
|
|
||
| As a first step for moving ``input_node`` and ``parallel_pipeline`` from ``flow_control``-body to a body | ||
| that uses optional, we can mark ``flow_control`` class as ``[[deprecated]]`` and temporarily support both forms. | ||
|
|
||
| In the implementation, we could do something like: | ||
|
|
||
| ```cpp | ||
| class [[deprecated]] flow_control { ... }; | ||
|
|
||
| // Part of input_node<Output> implementation | ||
|
|
||
| // Needed to avoid deprecation warning while creating flow_control | ||
| // Cross-platform warning suppression would be used | ||
| #pragma GCC diagnostic push | ||
| #pragma GCC diagnostic ignored "-Wdeprecated-declarations" | ||
|
|
||
| template <typename Body, typename = void> | ||
| struct body_uses_flow_control : std::false_type {}; // body uses optional | ||
|
|
||
| template <typename Body> | ||
| struct body_uses_flow_control<Body, | ||
| tbb::detail::void_t<decltype(tbb::detail::invoke(std::declval<Body>(), | ||
| std::declval<flow_control&>()))>> | ||
| : std::true_type {}; | ||
|
|
||
| template <typename Body> | ||
| void select_body(Body body, /*uses_flow_control = */std::true_type) { | ||
| tbb::flow_control fc; | ||
| Output output = body(fc); | ||
|
|
||
| while (!fc.is_pipeline_stopped) { | ||
| successors().try_put(output); | ||
| output = body(fc); | ||
| } | ||
| } | ||
|
|
||
| #pragma GCC diagnostic pop | ||
|
|
||
| template <typename Body> | ||
| void select_body(Body body, /*uses_flow_control = */std::false_type) { | ||
| auto output_opt = body(); | ||
| while (output_opt) { | ||
| successors().try_put(*output_opt); | ||
| output_opt = body(); | ||
| } | ||
| } | ||
|
|
||
| template <typename Body> | ||
| void generate_until_end_of_input(Body body) { // exposition-only | ||
| select_body(body, body_uses_flow_control<Body>{}); | ||
| } | ||
| ``` | ||
|
|
||
| Only the user code where a body taking ``flow_control`` is defined will generate a deprecation warning. | ||
| Library internal usage of ``flow_control`` suppresses the warning. | ||
|
|
||
| ## Possible Additional Applications to Consider | ||
|
|
||
| Some other TBB APIs can provide optional-aware alternatives. For example, ``try_pop(T&)`` in | ||
| ``concurrent_queue`` and ``concurrent_priority_queue`` could return ``tbb::optional``. | ||
| Also basic Flow Graph ``sender`` class functions ``try_get(T&)`` and ``try_reserve(T&)`` could | ||
| return ``tbb::optional`` (results in ABI break since the functions are virtual). | ||
|
|
||
| ## Open Questions | ||
|
|
||
| 1. How should ``tbb::optional`` and ``std::optional`` coexist in C++17? | ||
| See [C++17 Behavior](#c17-behavior) section for more detail. | ||
| 2. Which functions from ``std::optional`` should be included in ``tbb::optional`` implementation. | ||
| See [``tbb::optional`` API](#tbboptional-api) | ||
| and [Functionality of C++17 ``std::optional`` Excluded from ``tbb::optional``](#functionality-of-c17-stdoptionalexcluded-from-tbboptional) | ||
| sections for more detail. | ||
| 3. Can some requirements from ``std::optional`` be relaxed in ``tbb::optional``? | ||
| See [Functionality of C++17 ``std::optional`` Excluded from ``tbb::optional``](#functionality-of-c17-stdoptional-excluded-from-tbboptional) | ||
| section for more detail. | ||
| 4. Should ``tbb::optional::value`` throw exception for empty optional or be assert in debug, UB in release? | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
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 think it is worth mentioning, how would the body of one-filter pipeline look like. Something like
optional<continue_msg>could be used in this case:Perhaps moving
continue_msgout offlowshould be considered.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'm not sure I get the point about
continue_msg? Are you suggesting to movecontinue_msgout of the flow namespace or that we can dropcontinue_msgcompletely?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.
As far as I understand, the point is that Optional-Like cannot be used for single pipeline filter (both input and output are
void):The idea is to use
continue_msgas a special flag class in this case and return non-emptyoptional<continue_msg>if the pipeline should be continued and an empty optional if the pipeline should be stopped.Since
continue_msgis part offlownamespace now, the idea is to move it totbbnamespace. However, since we need to keep the old code that usestbb::flow::continue_msg, I guess having it in bothtbbandtbb::flowis a better option.An alternative is to keep
flow_controlfor single-filter (or even for entireparallel_pipeline) and to migrate to Optional-Like only forinput_node.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.
Ok, I see now. For this use, the semantics of
continue_msgis different. Usually, it means that the predecessor-successor dependence has been satisfied between two nodes. If used to signal the continue/stop condition for a single filter pipeline, it would be acting as-if there is an imaginary back edge from the single filter to itself. You call it a "special flag class". I wonder if any savings from eliminating an extra class is worth the possible sematic confusion?