Skip to content

Commit db04d1c

Browse files
authored
Merge pull request #1733 from ericniebler/deprecate-tag-invoke
deprecate the use of `tag_invoke` in all the CPOs
2 parents 250f357 + 8b3359d commit db04d1c

38 files changed

+299
-255
lines changed

MAINTAINERS.md

Lines changed: 26 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -3,30 +3,12 @@
33
This is a place to put design rationale and code idioms that maintainers of
44
stdexec should follow.
55

6-
## ADL isolation
6+
## Historical reason for all the nested `__t` structs
77

8-
Stdexec makes use of the [`tag_invoke`](http://wg21.link/p1895) mechanism to
9-
support end-user customization of algorithms. For the sake of compile-time
10-
performance, this mechanism requires care to ensure that overload sets stay
11-
small.
12-
13-
### Hidden friend functions
14-
15-
Define `tag_invoke` overloads for class types _in-situ_ as hidden friend
16-
functions. Keep in mind that when calling a customizable funtion that
17-
all the associated entities (classes and namespaces) of all the function's
18-
arguments are searched for `tag_invoke` overloads. That means that for
19-
a function call like:
20-
21-
```c++
22-
connect(sender, receiver);
23-
```
24-
25-
... the hidden friend functions of the _receiver_ will be considered in
26-
addition to those of the sender. This can sometimes lead to surprising and
27-
confusing compiler errors.
28-
29-
### Class template parameters
8+
At one point all the CPOs used `tag_invoke`, which meant they all did
9+
argument-dependent lookup (ADL) under the covers. ADL searches all of
10+
a type's associated namespaces and classes when looking for the best
11+
overload.
3012

3113
For a class template instantiation such as `N::S<A,B,C>`, the associated
3214
entities include the associated entities of `A`, `B`, and `C`. This is
@@ -46,27 +28,27 @@ than defining a sender adaptor as:
4628

4729
```c++
4830
template <class Sender, class Arg>
49-
struct my_sender {
50-
Sender sndr_;
51-
Arg arg_;
52-
// ... rest of sender implementation
53-
};
31+
struct my_sender {
32+
Sender sndr_;
33+
Arg arg_;
34+
// ... rest of sender implementation
35+
};
5436
```
5537
5638
we define it as follows:
5739
5840
```c++
5941
template <class SenderId, class Arg>
60-
struct my_sender_id {
61-
using Sender = stdexec::__t<SenderId>;
62-
63-
struct __t {
64-
using __id = my_sender_id;
65-
Sender sndr_;
66-
Arg arg_;
67-
// ... rest of sender implementation
68-
};
42+
struct my_sender_id {
43+
using Sender = stdexec::__t<SenderId>;
44+
45+
struct __t {
46+
using __id = my_sender_id;
47+
Sender sndr_;
48+
Arg arg_;
49+
// ... rest of sender implementation
6950
};
51+
};
7052
7153
template <class Sender, class Arg>
7254
using my_sender =
@@ -82,7 +64,7 @@ to get back the original type from the identifier.
8264
> We only really need to encode the type of the template arguments that
8365
> are likely to have `tag_invoke` overloads that we want to exclude when
8466
> looking for overloads for _this_ type. Hence, the `Sender` type is
85-
> encoded but the `Arg` type is left alone.
67+
> encoded but the `Arg` type is left alone.
8668

8769
Additionally, we move the implementation of the type into a (non-template)
8870
nested class type called `__t`. This nested class type must have a nested
@@ -92,16 +74,20 @@ When these guidelines are followed, we can ensure that the minimum number
9274
of class templates are instantiated, and the types of composite senders
9375
remains short, uncluttered, and readable.
9476

77+
But as the use of `tag_invoke` in stdexec has been deprecated, we can
78+
stop worrying about this issue in the future, when support for it has
79+
been dropped completely.
80+
9581
## Assorted tips and tricks
9682

9783
* Data, including downstream receivers, are best stored in the operation
9884
state. Receivers themselves should, in general, store nothing but a
9985
pointer back to the operation state.
10086
* Assume that schedulers and receivers contain nothing but a pointer and
10187
are cheap to copy. Take them by value.
102-
* All `tag_invoke` overloads _must_ be constrained.
88+
* Do not use `tag_invoke` anywhere. It's deprecated.
10389
* In a sender adaptor, a reasonable way to constrain
104-
`tag_invoke(connect_t, ThisSender<InnerSender>, OuterReceiver)` is by
90+
`ThisSender<InnerSender>::connect(OuterReceiver)` is by
10591
requiring `sender_to<InnerSender, ThisReceiver<OuterReceiver>>`.
10692
* Place concept checks on public interfaces. Only place concept checks
10793
on implementation details if needed for correctness; otherwise, leave

examples/algorithms/retry.hpp

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -131,8 +131,8 @@ struct _retry_sender {
131131
}
132132

133133
template <stdexec::receiver R>
134-
friend auto tag_invoke(stdexec::connect_t, _retry_sender&& self, R r) -> _op<S, R> {
135-
return {static_cast<S&&>(self.s_), static_cast<R&&>(r)};
134+
auto connect(R r) && -> _op<S, R> {
135+
return {static_cast<S&&>(s_), static_cast<R&&>(r)};
136136
}
137137

138138
auto get_env() const noexcept -> stdexec::env_of_t<S> {

examples/benchmark/fibonacci.cpp

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -67,8 +67,9 @@ struct fib_s {
6767
};
6868

6969
template <stdexec::receiver_of<completion_signatures> Receiver>
70-
friend auto tag_invoke(stdexec::connect_t, fib_s self, Receiver rcvr) -> operation<Receiver> {
71-
return {static_cast<Receiver&&>(rcvr), self.cutoff, self.n, self.sched};
70+
[[nodiscard]]
71+
auto connect(Receiver rcvr) const -> operation<Receiver> {
72+
return {static_cast<Receiver&&>(rcvr), cutoff, n, sched};
7273
}
7374
};
7475

examples/benchmark/static_thread_pool_old.hpp

Lines changed: 5 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -169,18 +169,12 @@ namespace exec_old {
169169
using completion_signatures =
170170
stdexec::completion_signatures<stdexec::set_value_t(), stdexec::set_stopped_t()>;
171171

172-
private:
173-
template <typename Receiver>
174-
auto make_operation_(Receiver r) const -> operation<stdexec::__id<Receiver>> {
175-
return operation<stdexec::__id<Receiver>>{pool_, static_cast<Receiver&&>(r)};
176-
}
177-
178172
template <stdexec::receiver Receiver>
179-
friend auto tag_invoke(stdexec::connect_t, sender s, Receiver r)
180-
-> operation<stdexec::__id<Receiver>> {
181-
return s.make_operation_(static_cast<Receiver&&>(r));
173+
auto connect(Receiver r) const -> operation<stdexec::__id<Receiver>> {
174+
return operation<stdexec::__id<Receiver>>{pool_, static_cast<Receiver&&>(r)};
182175
}
183176

177+
private:
184178
struct env {
185179
static_thread_pool& pool_;
186180

@@ -481,7 +475,7 @@ namespace exec_old {
481475
Receiver,
482476
completion_signatures<Self, stdexec::env_of_t<Receiver>>
483477
>
484-
friend auto tag_invoke(stdexec::connect_t, Self&& self, Receiver rcvr)
478+
STDEXEC_EXPLICIT_THIS_BEGIN(auto connect)(this Self&& self, Receiver rcvr)
485479
noexcept(stdexec::__nothrow_constructible_from<
486480
bulk_op_state_t<Self, Receiver>,
487481
static_thread_pool&,
@@ -497,6 +491,7 @@ namespace exec_old {
497491
static_cast<Self&&>(self).sndr_,
498492
static_cast<Receiver&&>(rcvr)};
499493
}
494+
STDEXEC_EXPLICIT_THIS_END(connect)
500495

501496
template <stdexec::__decays_to<bulk_sender> Self, class Env>
502497
STDEXEC_EXPLICIT_THIS_BEGIN(auto get_completion_signatures)(this Self&&, Env&&)

examples/retry.cpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ struct fail_some {
4646
};
4747

4848
template <class R>
49-
friend auto tag_invoke(stdexec::connect_t, fail_some, R r) -> op<R> {
49+
auto connect(R r) const -> op<R> {
5050
return {std::move(r)};
5151
}
5252
};

include/exec/sequence_senders.hpp

Lines changed: 54 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -134,8 +134,9 @@ namespace exec {
134134
}
135135

136136
template <receiver _Receiver, sender _Item>
137-
requires(!__has_set_next_member<_Receiver, _Item>)
138-
&& tag_invocable<set_next_t, _Receiver&, _Item>
137+
requires __has_set_next_member<_Receiver, _Item>
138+
|| tag_invocable<set_next_t, _Receiver&, _Item>
139+
[[deprecated("the use of tag_invoke for set_next is deprecated")]]
139140
auto operator()(_Receiver& __rcvr, _Item&& __item) const
140141
noexcept(nothrow_tag_invocable<set_next_t, _Receiver&, _Item>)
141142
-> tag_invoke_result_t<set_next_t, _Receiver&, _Item> {
@@ -298,8 +299,8 @@ namespace exec {
298299
} else if constexpr (__is_debug_env<_Env>) {
299300
using __tag_invoke::tag_invoke;
300301
// This ought to cause a hard error that indicates where the problem is.
301-
using __item_types_t [[maybe_unused]] =
302-
decltype(__declval<__tfx_sequence_t>().get_item_types(__declval<_Env>()));
302+
using __item_types_t [[maybe_unused]] = decltype(__declval<__tfx_sequence_t>()
303+
.get_item_types(__declval<_Env>()));
303304
return static_cast<__debug::__item_types (*)()>(nullptr);
304305
} else {
305306
using __result_t = __unrecognized_sequence_error_t<_Sequence, _Env>;
@@ -560,6 +561,7 @@ namespace exec {
560561
concept __subscribable_with_tag_invoke = tag_invocable<subscribe_t, _Sequence, _Receiver>;
561562

562563
struct subscribe_t {
564+
private:
563565
template <class _Sequence, class _Receiver>
564566
STDEXEC_ATTRIBUTE(always_inline)
565567
static constexpr auto __type_check_arguments() -> bool {
@@ -588,10 +590,8 @@ namespace exec {
588590

589591
template <class _Sequence, class _Receiver>
590592
static constexpr auto __get_declfn() noexcept {
591-
using __domain_t =
592-
stdexec::__detail::__completing_domain_t<set_value_t, _Sequence, env_of_t<_Receiver&>>;
593593
constexpr bool __nothrow_tfx_sequence =
594-
__nothrow_callable<transform_sender_t, __domain_t, _Sequence, env_of_t<_Receiver>>;
594+
__nothrow_callable<transform_sender_t, _Sequence, env_of_t<_Receiver>>;
595595
using __tfx_sequence_t = __transform_sender_result_t<_Sequence, _Receiver>;
596596

597597
static_assert(
@@ -631,13 +631,6 @@ namespace exec {
631631
&& noexcept(__declval<__tfx_sequence_t>()
632632
.subscribe(__declval<_Receiver>()));
633633
return __declfn<__result_t, __is_nothrow>();
634-
} else if constexpr (__subscribable_with_tag_invoke<__tfx_sequence_t, _Receiver>) {
635-
using __result_t = tag_invoke_result_t<subscribe_t, __tfx_sequence_t, _Receiver>;
636-
__check_operation_state<__result_t>();
637-
constexpr bool __is_nothrow =
638-
__nothrow_tfx_sequence
639-
&& nothrow_tag_invocable<subscribe_t, __tfx_sequence_t, _Receiver>;
640-
return __declfn<__result_t, __is_nothrow>();
641634
} else if constexpr (__is_debug_env<env_of_t<_Receiver>>) {
642635
using __result_t = __debug::__debug_operation;
643636
return __declfn<__result_t, __nothrow_tfx_sequence>();
@@ -649,9 +642,13 @@ namespace exec {
649642
}
650643
}
651644

652-
template <sender _Sequence,
653-
receiver _Receiver,
654-
auto _DeclFn = __get_declfn<_Sequence, _Receiver>()>
645+
public:
646+
template <
647+
sender _Sequence,
648+
receiver _Receiver,
649+
auto _DeclFn = __get_declfn<_Sequence, _Receiver>()
650+
>
651+
requires stdexec::__callable<decltype(_DeclFn)>
655652
auto operator()(_Sequence&& __sequence, _Receiver&& __rcvr) const
656653
noexcept(noexcept(_DeclFn())) -> decltype(_DeclFn()) {
657654
using __tfx_sequence_t = __transform_sender_result_t<_Sequence, _Receiver>;
@@ -661,9 +658,8 @@ namespace exec {
661658

662659
if constexpr (__next_connectable<__tfx_sequence_t, _Receiver>) {
663660
// sender as sequence of one
664-
next_sender_of_t<_Receiver, __tfx_sequence_t> __next = set_next(
665-
__rcvr,
666-
static_cast<__tfx_sequence_t&&>(__tfx_sequence));
661+
next_sender_of_t<_Receiver, __tfx_sequence_t> __next =
662+
set_next(__rcvr, static_cast<__tfx_sequence_t&&>(__tfx_sequence));
667663
return stdexec::connect(
668664
static_cast<next_sender_of_t<_Receiver, __tfx_sequence_t>&&>(__next),
669665
__stopped_means_break_t<_Receiver>{static_cast<_Receiver&&>(__rcvr)});
@@ -674,11 +670,6 @@ namespace exec {
674670
} else if constexpr (__subscribable_with_member<__tfx_sequence_t, _Receiver>) {
675671
return static_cast<__tfx_sequence_t&&>(__tfx_sequence)
676672
.subscribe(static_cast<_Receiver&&>(__rcvr));
677-
} else if constexpr (__subscribable_with_tag_invoke<__tfx_sequence_t, _Receiver>) {
678-
return stdexec::tag_invoke(
679-
subscribe_t{},
680-
static_cast<__tfx_sequence_t&&>(__tfx_sequence),
681-
static_cast<_Receiver&&>(__rcvr));
682673
} else if constexpr (enable_sequence_sender<stdexec::__decay_t<__tfx_sequence_t>>) {
683674
// sequence sender fallback
684675

@@ -691,15 +682,50 @@ namespace exec {
691682

692683
// This should generate an instantiate backtrace that contains useful
693684
// debugging information.
694-
next_sender_of_t<_Receiver, __tfx_sequence_t> __next = set_next(
695-
__rcvr,
696-
static_cast<__tfx_sequence_t&&>(__tfx_sequence));
685+
next_sender_of_t<_Receiver, __tfx_sequence_t> __next =
686+
set_next(__rcvr, static_cast<__tfx_sequence_t&&>(__tfx_sequence));
697687
return stdexec::connect(
698688
static_cast<next_sender_of_t<_Receiver, __tfx_sequence_t>&&>(__next),
699689
__stopped_means_break_t<_Receiver>{static_cast<_Receiver&&>(__rcvr)});
700690
}
701691
}
702692

693+
template <
694+
sender _Sequence,
695+
receiver _Receiver,
696+
auto _DeclFn = __get_declfn<_Sequence, _Receiver>()
697+
>
698+
requires stdexec::__callable<decltype(_DeclFn)>
699+
|| stdexec::tag_invocable<
700+
subscribe_t,
701+
__transform_sender_result_t<_Sequence, _Receiver>,
702+
_Receiver
703+
>
704+
[[deprecated("the use of tag_invoke for subscribe is deprecated")]]
705+
auto operator()(_Sequence&& __sequence, _Receiver&& __rcvr) const noexcept(
706+
__nothrow_callable<transform_sender_t, _Sequence, env_of_t<_Receiver>>
707+
&& stdexec::nothrow_tag_invocable<
708+
subscribe_t,
709+
__transform_sender_result_t<_Sequence, _Receiver>,
710+
_Receiver
711+
>)
712+
-> stdexec::tag_invoke_result_t<
713+
subscribe_t,
714+
__transform_sender_result_t<_Sequence, _Receiver>,
715+
_Receiver
716+
> {
717+
using __tfx_sequence_t = __transform_sender_result_t<_Sequence, _Receiver>;
718+
using __result_t = tag_invoke_result_t<subscribe_t, __tfx_sequence_t, _Receiver>;
719+
__check_operation_state<__result_t>();
720+
auto&& __env = stdexec::get_env(__rcvr);
721+
auto&& __tfx_sequence =
722+
stdexec::transform_sender(static_cast<_Sequence&&>(__sequence), __env);
723+
return stdexec::tag_invoke(
724+
subscribe_t{},
725+
static_cast<__tfx_sequence_t&&>(__tfx_sequence),
726+
static_cast<_Receiver&&>(__rcvr));
727+
}
728+
703729
static constexpr auto query(stdexec::forwarding_query_t) noexcept -> bool {
704730
return false;
705731
}

include/stdexec/__detail/__as_awaitable.hpp

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -210,10 +210,6 @@ namespace stdexec {
210210
using _Result = decltype(__declval<_Tp>().as_awaitable(__declval<_Promise&>()));
211211
constexpr bool _Nothrow = noexcept(__declval<_Tp>().as_awaitable(__declval<_Promise&>()));
212212
return __declfn<_Result, _Nothrow>();
213-
} else if constexpr (tag_invocable<as_awaitable_t, _Tp, _Promise&>) {
214-
using _Result = tag_invoke_result_t<as_awaitable_t, _Tp, _Promise&>;
215-
constexpr bool _Nothrow = nothrow_tag_invocable<as_awaitable_t, _Tp, _Promise&>;
216-
return __declfn<_Result, _Nothrow>();
217213
// NOLINTNEXTLINE(bugprone-branch-clone)
218214
} else if constexpr (__awaitable<_Tp, __unspecified>) { // NOT __awaitable<_Tp, _Promise> !!
219215
return __declfn<_Tp&&>();
@@ -228,16 +224,13 @@ namespace stdexec {
228224
}
229225

230226
template <class _Tp, class _Promise, auto _DeclFn = __get_declfn<_Tp, _Promise>()>
227+
requires __callable<__mtypeof<_DeclFn>>
231228
auto operator()(_Tp&& __t, _Promise& __promise) const noexcept(noexcept(_DeclFn()))
232229
-> decltype(_DeclFn()) {
233230
if constexpr (__has_as_awaitable_member<_Tp, _Promise>) {
234231
using _Result = decltype(static_cast<_Tp&&>(__t).as_awaitable(__promise));
235232
static_assert(__awaitable<_Result, _Promise>);
236233
return static_cast<_Tp&&>(__t).as_awaitable(__promise);
237-
} else if constexpr (tag_invocable<as_awaitable_t, _Tp, _Promise&>) {
238-
using _Result = tag_invoke_result_t<as_awaitable_t, _Tp, _Promise&>;
239-
static_assert(__awaitable<_Result, _Promise>);
240-
return tag_invoke(*this, static_cast<_Tp&&>(__t), __promise);
241234
// NOLINTNEXTLINE(bugprone-branch-clone)
242235
} else if constexpr (__awaitable<_Tp, __unspecified>) { // NOT __awaitable<_Tp, _Promise> !!
243236
return static_cast<_Tp&&>(__t);
@@ -248,6 +241,17 @@ namespace stdexec {
248241
return static_cast<_Tp&&>(__t);
249242
}
250243
}
244+
245+
template <class _Tp, class _Promise, auto _DeclFn = __get_declfn<_Tp, _Promise>()>
246+
requires __callable<__mtypeof<_DeclFn>> || tag_invocable<as_awaitable_t, _Tp, _Promise&>
247+
[[deprecated("the use of tag_invoke for as_awaitable is deprecated")]]
248+
auto operator()(_Tp&& __t, _Promise& __promise) const
249+
noexcept(nothrow_tag_invocable<as_awaitable_t, _Tp, _Promise&>)
250+
-> tag_invoke_result_t<as_awaitable_t, _Tp, _Promise&> {
251+
using _Result = tag_invoke_result_t<as_awaitable_t, _Tp, _Promise&>;
252+
static_assert(__awaitable<_Result, _Promise>);
253+
return tag_invoke(*this, static_cast<_Tp&&>(__t), __promise);
254+
}
251255
};
252256
} // namespace __as_awaitable
253257

0 commit comments

Comments
 (0)