Skip to content

Introduce exec::function<...>#2040

Open
ispeters wants to merge 37 commits intoNVIDIA:mainfrom
ispeters:frame_allocator
Open

Introduce exec::function<...>#2040
ispeters wants to merge 37 commits intoNVIDIA:mainfrom
ispeters:frame_allocator

Conversation

@ispeters
Copy link
Copy Markdown
Contributor

This PR proposes a new type-erased sender named exec::function. There's an in-code comment giving a bunch of examples, but a simple example is:

exec::function<int(int)> fn(42, [](int i) { return ex::just(i); });

auto [result] = ex::sync_wait(std::move(fn)).value();

assert(result == 42);

There are a bunch of TODOs left, including lots of tests that are missing, but the API is ready to collect early feedback. If this looks like a promising direction, I intend to write a paper for Brno proposing this type for inclusion in C++29.

@copy-pr-bot
Copy link
Copy Markdown

copy-pr-bot Bot commented Apr 21, 2026

This pull request requires additional validation before any workflows can run on NVIDIA's runners.

Pull request vetters can view their responsibilities here.

Contributors can view more details about this message here.

Copy link
Copy Markdown
Collaborator

@ericniebler ericniebler left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why capture a function and arguments instead of just a sender that aggregates the arguments? what does lazy construction of the sender offer here?

could this be implemented in terms of:

template <class Result,
          class ReceiverQueries = queries<>,
          class Completions = completion_signatures<set_error_t(exception_ptr),
                                                    set_stopped_t()>,
          class SenderQueries = queries<>>
auto function(auto sndr) 
{
  using _completions_t =
    __minvoke<__mpush_back<__q<completion_signatures>>, Completions, set_value_t(Result)>;

  using _sender_t =
    any_sender<any_receiver<_completions_t, ReceiverQueries>, SenderQueries>;

   return _sender_t(let_value(read_env(get_frame_allocator),
                              [=](auto const& alloc)
                              {
                                return __uses_frame_allocator(sndr, alloc);
                              }));
}

EDIT: also, the exec::function interface suggests to me that it would be used like:

exec::function<int(int)> fn([](int i) { return ex::just(i); });

auto [result] = ex::sync_wait(fn(42)).value();

the lazy construction of the sender would then makes sense.

Comment thread include/exec/function.hpp Outdated
Comment thread include/exec/function.hpp Outdated
Comment thread include/exec/function.hpp Outdated
Comment thread include/exec/function.hpp Outdated
Comment thread include/exec/function.hpp Outdated
Comment thread include/exec/function.hpp Outdated
Comment thread include/exec/function.hpp Outdated
Comment thread include/exec/function.hpp Outdated
Comment thread include/exec/function.hpp Outdated
Comment thread include/exec/function.hpp Outdated
Copy link
Copy Markdown
Collaborator

@ericniebler ericniebler left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i still don't understand why this utility captures args and a function that returns a sender instead of just capturing the resulting sender. what benefit do we gain from lazily constructing the sender at connect time?

Comment thread include/exec/function.hpp Outdated
Comment thread include/exec/function.hpp Outdated
Comment thread include/exec/function.hpp Outdated
Comment thread include/exec/function.hpp Outdated
@ericniebler
Copy link
Copy Markdown
Collaborator

/ok to test bcdaae2

@ispeters
Copy link
Copy Markdown
Contributor Author

i still don't understand why this utility captures args and a function that returns a sender instead of just capturing the resulting sender. what benefit do we gain from lazily constructing the sender at connect time?

Yeah, sorry, I was responding to the easier feedback before getting to this philosophical question and it's not surprising you have this question—somewhere between having the idea for this PR and actually publishing it, I got distracted by the details and didn't include a very good motivation. Let me try to fix that.

The back story is that I'm looking into the benchmark that Vinnie and Steve have published here. I'm still working on developing a deep enough understanding of the benchmark to have intelligent opinions about it but my initial impression is that the coroutine code is very well written (unsurprising given the author's expertise), but the sender code is unidiomatic, suggesting to me that the author is less experienced with senders than with coroutines. I'd like to improve the sender code to make the comparison between approaches as fair as possible.

One of the apparent weaknesses of senders that is exposed in the benchmark is that it's more difficult to optimize the allocation patterns of an any_sender_of than that of a coroutine task type (capy::task in the benchmark, but I think it generalizes). There are probably several ways to tackle this problem, but capturing arguments plus a sender factory is an idea I had while ruminating on the issue that I think could prove very useful.

The idea behind this exec::function is that it's sort of a "better coroutine". By capturing an arg tuple + sender factory, the initial sender never has to allocate, although, because we're still doing type erasure, we obviously have to support allocating the operation state modulo any small buffer optimization. Ensuring the opstate allocation happens through the newly-introduced "frame allocator", lets this type benefit from the same allocation optimizations that Vinnie and co. have made in Capy. If the frame allocator is what Vinnie calls a "recycling allocator" then the stack-like nature of nested operations can be reflected in a stack-like allocation pattern that leads to amortized zero allocations per operation.

The usage I expect would look like this:

struct interface {
  virtual exec::function<int(interface*) noexcept> get_int() noexcept = 0;
};

struct impl : interface {
  exec::function<int(interface*) noexcept> get_int() noexcept override {
    return exec::function<int(interface*) noexcept>(this, [](interface* base) noexcept {
      auto* self = static_cast<impl*>(base);
      // presumably some more interesting composition of senders in practice
      return ex::just(self->i_);
    });
  }

 private:
  int i_;
};

Letting myself think grandiose thoughts, I could imagine extending this with a language feature that lets you use coroutine syntax to let the compiler write the actual sender for you, like this:

exec::function<std::string(int)> async_to_string(int i) {
  co_return std::format("{}", i);
}

For cases where async_to_string is declared in a header and defined in a .cpp file, the type erasure, with the concomitant allocation, can't really be avoided, but for cases where the compiler can see the body of async_to_string at the call site, I could imagine the compiler generating a parent operation that has space in its opstate for the child opstate, eliding any allocations. Imagining even further, it'd be nice to have an "auto"-like syntax for inline async functions where the compiler not only generates the sender implementation, but also computes the full set of completion signatures to make the declaration more terse. Maybe something like

exec::function<ex::sender_tag> async_to_string(int i) {
  // compiler computes set_value_t(std::string) and set_error_t(std::exception_ptr)
  // but not set_stopped_t() because this coroutine doesn't complete with stopped
  co_return std::format("{}", i);
}

@ericniebler
Copy link
Copy Markdown
Collaborator

ericniebler commented Apr 30, 2026

By capturing an arg tuple + sender factory, the initial sender never has to allocate

so the allocation this saves is the one that can potentially happen when type-erasing a sender? could you achieve the same thing with a (non-type-erasing) sender adaptor that returns a type-erased opstate from connect?

@ericniebler
Copy link
Copy Markdown
Collaborator

/ok to test ede4ba2

@ispeters
Copy link
Copy Markdown
Contributor Author

By capturing an arg tuple + sender factory, the initial sender never has to allocate

so the allocation this saves is the one that can potentially happen when type-erasing a sender?

Yes.

could you achieve the same thing with a (non-type-erasing) sender adaptor that returns a type-erased opstate from connect?

I don't think so, but maybe you can see a way? Suppose we had sender auto exec::type_erase(sender auto&&); I think you can't get around the fact that the input sender is of arbitrary size so either the return type has to fully specify the type of the input plus whatever wrapping it does, or you have to type-erase the input and risk an eager allocation.

Also, fwiw, I'm open to feedback on the name of this type. If you look at the commit history, you can see I originally named this thing capy_sender because the immediate application is the Capy-sourced benchmark. Then, I thought io_sender would be a better name since that's what Vinnie claims this approach is best for. I finally landed on function because:

  • Robert's description of how senders work describes how you first curry your arguments into a sender, then you allocate a frame in the form of an operation state, and then you actually execute the async function by invoking start so this type implements a kind of platonic sender—curry, allocate, execute—which suggests that it's a type-erased async function; and
  • Robert has also said that he believes everything in the std::execution namespace is presumptively async so std::execution::async_function<void()> is redundant—we can just have std::execution::function<void()>.

That said, the confusion you expressed earlier about the interface suggests to me that the name might be a problem. Perhaps something like curried<void()>, sender_factory<void()>.

@ericniebler
Copy link
Copy Markdown
Collaborator

Suppose we had sender auto exec::type_erase(sender auto&&); I think you can't get around the fact that the input sender is of arbitrary size so either the return type has to fully specify the type of the input plus whatever wrapping it does

yes, that is what i was asking about. what is the problem with doing an ordinary sender adaptor?

@ericniebler
Copy link
Copy Markdown
Collaborator

/ok to test fa12a41

@ispeters
Copy link
Copy Markdown
Contributor Author

ispeters commented May 1, 2026

Suppose we had sender auto exec::type_erase(sender auto&&); I think you can't get around the fact that the input sender is of arbitrary size so either the return type has to fully specify the type of the input plus whatever wrapping it does

yes, that is what i was asking about. what is the problem with doing an ordinary sender adaptor?

I'm not certain whether you're asking, "why is function a type instead of an algorithm?", or "why does this thing—whether type or algorithm—have to type-erase a factory instead of adapting a concrete sender?"

Regarding "why a type?", I'm not really fussed either way, except that the motivating use case requires that users be able to spell a type so they can return it from functions declared-but-not-defined in headers. So if you're looking for an algorithm to be the "constructor" instead of using a regular type with a constructor, that's fine by me, but I think there needs to be a type regardless.

Regarding "why does this type-erase the factory?", I can't see another way to achieve my goal, but I'm open to counterexamples. I want to be able to write this sort of thing:

// iface.hpp
#include <memory>

struct iface {
  virtual ~iface() = default;

  virtual $type$ async_virtual(int idx) = 0;
};

std::unique_ptr<iface> make_iface();

// impl.hpp
#include "iface.hpp"

struct impl : iface {
  $type$ async_virtual(int idx) override;

  $type$ async_member();
};

// impl.cpp
#include "impl.hpp"

#include <execution>

sender auto some_sender(int idx) {
  // some arbitrary composition of senders
}

sender auto other_sender() {
  // some other arbitrary composition of senders
}

$type$ impl::async_virtual(int idx) {
  // return some function of some_sender(idx);
}

$type$ impl::async_member() {
  // return some function of other_sender();
}

std::unique_ptr<iface> make_iface() {
  return std::unique_ptr<impl>();
}

// main.cpp
#include "impl.hpp":

int main() {
  auto p = make_iface();

  sync_wait(p->async_virtual(42));

  auto* q = static_cast<impl*>(p.get());

  // async_member's declaration is visible, but not its definition
  sync_wait(q->async_member());
}

If we imagine we have sender auto type_erase(sender auto&&) and use it in impl.cpp, I think you have this:

$type$ impl::async_virtual(int idx) {
  return type_erase(some_sender(idx));
}

$type$ impl::async_member() {
  return type_erase(other_sender());
}

Given the declaration of some_sender and other_sender are not visible outside impl.cpp, main.cpp doesn't know how big the concrete sender returned from type_erase is, so I think you inevitably have to do the standard small buffer-optimized type-erasure dance and risk an allocation. The only alternative I can see is for the specific types of the composed senders to leak out of the interface through something equivalent to using $type$ = decltype(type_erase(...));, which I think makes it inappropriate for use as the return type of virtual member functions, or separately-compiled functions of any kind.

If $type$ is an instantiation of the proposed function, then impl.cpp looks like this (assuming the senders returned from some_sender and other_sender complete with set_value_t() for simplicity):

exec::function<void(int)> impl::async_virtual(int idx) {
  return exec::function<void(int)>(
      std::move(idx), // it bugs me that this move is required in the current code…
      [](int idx) { return some_sender(idx); });
}

exec::function<void()> impl::async_member() {
  return exec::function<void()>([] { return other_sender(); });
}

The type-erased sender factory is of constant size (because I restrict the factory to be trivially-copyable types no bigger than two pointers to accommodate regular function pointers and member function pointers), and the storage for the factory's arguments is knowable and a function of the class template parameters because they're expressed in the function type.

Does that answer your question? Do you see another way to accomplish the goal? A function<int(???)> is like a task<int> but with better-timed allocation because it erases less.

@ispeters ispeters force-pushed the frame_allocator branch from fa12a41 to 9c4d030 Compare May 1, 2026 18:55
@ericniebler
Copy link
Copy Markdown
Collaborator

thanks, i understand now what problem you're solving. it's very interesting. 🤔

@ericniebler
Copy link
Copy Markdown
Collaborator

/ok to test 025628b

ispeters added 14 commits May 3, 2026 09:33
This diff starts the work to add a type-erased sender named
`io_sender<Return(Args...)>`. The intent is for such a sender to
represent "an async function from `Args...` to `Return`", a bit like a
task coroutine, but with different trade offs. The sender itself stores
a `std::tuple<Args...>` and a `sender auto(Args&&...)` factory that can
construct the intended erased sender from the stored arguments on
demand. This representation allows us to defer allocation of the
type-erased operation state until `connect` time, giving us
coroutine-like behaviour but allowing us to choose the frame allocator
by querying the eventual receiver's environment.

The completion signatures for an `io_sender<Return(Args...)>` are:
 - `set_value_t(R&&)`
 - `set_error_t(std::exception_ptr)`
 - `set_stopped_t()`

We may be able to eliminate the error channel for
`io_sender<R(A...) noexcept>` but that direction requires more thought.

This first diff proves that we can store a tuple of arguments and a
factory and, at `connect` time, use those values to allocate a
type-erased operation state. The test cases cover only basic cases, and
all allocations happen through `::operator new`. Future changes will
expand the test cases and invent a `get_frame_allocator` environment
query that can be used to control frame allocations. The expectation is
that we can meet Capy's performance characteristics with a slightly
different API in a sender-first way.
This diff changes the name of `io_sender<R(A...)>` to
`function<R(A...)>` after some discussion with other folks working on
`std::execution`. `exec::function<...>` is a type-erased wrapper around
an async function with the given signature (elided here as `...`). More
features are coming in future diffs.
Move to an implementation that spreads `completion_signatures`
throughout the internals so that we're not restricted to `R(A...)`-style
constraints.

The tests still only validate `R(A...)`-style constraints, with no
validation of no-throw functions, or controlling the completion
signature and environment; that'll come next.

This implementation also relies on virtual inheritance of a pack of
abstract base classes, which feels like a kludge. I should figure out
how to reimplement the virtual dispatch in terms of a hand-rolled
vtable.
Thanks to a suggestion from @RobertLeahy, I've been able to rework the
virtual function inheritance to not need virtual inheritance.
`function<ex::sender_tag(Args...), ex::completion_signatures<...>>` now
declares an async function mapping `Args...` to the explicitly specified
completion signatures.
Support for explicit completion signatures, environment, or both in the
declaration of an `exec:function`.
Rework the dynamically allocated operation state type to support
allocators, but always use `std::allocator` for now.
This diff needs tests, but the existing tests build and pass, which
seems like a good signal. I've added a `get_frame_allocator` query, and
a defaulting cascade from `get_frame_allocator` -> `get_allocator` ->
`std::allocator<std::byte>` to the allocation of `_derived_op`.
This diff marks almost every function `constexpr`. It doesn't mark the
imlementation of `complete` in the CRTP `_func_op_completion` class
template because Clang rejects the down-cast to `Derived` as not a
core constant expression; apparently, `Derived` is incomplete when it's
being evaluated as a side effect of constraint satisfaction testing.

This `constexpr` "hole" means `exec::function` can't be used at compile
time, but maybe it can be worked around later.
Validate that more kinds of senders can be erased and then connected and
started. Also clean up the captures in some lambdas in `connect` and
`clang-format`.
Still TODO is that the `get_frame_allocator` query shouldn't have to be
specified in the `function`'s custom environment (and, come to think of
it, neither should `get_allocator`), but, when specified, it works.
This needs cleaning up and a *lot* more tests, but the current tests
build and pass with a synthesized polymorphic environment.
ispeters added 22 commits May 3, 2026 09:33
Per code review feedback, replace `[[no_unique_address]]` with
`STDEXEC_ATTRIBUTE(no_unique_address)`.
Take @ericniebler's code review feedback to clean up the declaration of
`exec::_func::_func_impl`'s constructor.
This commit replaces the vtable-building shenanigans in `exec::function`
with the `exec::_any::_any_receiver_ref` class template in
`any_sender_of.hpp`. The comments probably still need cleaning up, and
there's a `TODO` to pull the stuff in `any_sender_of.hpp` that's shared
between `exec::any_sender_of` and `exec::function` into a separate,
shared header.
Update comments to match the new implementation.
Take code review feedback and replace attempts to deduce a function
type's `noexcept` clause with explicit partial specializations for both
the throwing and non-throwing cases.
Replace the `unique_ptr` to custom type-erased operation state with an
`STDEXEC::__any::__any<exec::_any::_iopstate>`; I might be able to go
further and replace `_func_op` with `exec::_any::_any_opstate`, but I
need to think about the stop token adaption it does before committing to
that.
This change moves `_func::_func_op` to store its receiver as an
`_any::_state`, and its child op as an `_any::_any_opstate_base`,
similar to how `_any::_any_opstate` works. This means there's now
support for adapting stop tokens, and it slightly shortens some
declarations because `_any_opstate_base` is shorter than
`__any::__any<_any::_iopstate>`.
Take @ericniebler's suggestion and simplify the `_sigs_from_t` alias
template.
Clean up the implementation of `connect`:
* switch from `std::tuple` to `STDEXEC::__tuple`
* rvalue ref-qualify the existing `connect`
* add a const lvalue ref-qualified `connect` that copies the source
  sender and rvalue connects the temporary
* add a test of lvalue connect
Add some descriptive `SECTION("blah")` declarations to the basic tests.
Add a test proving that @ericniebler's suggestion to deduce `function`'s
factory argument as a value is necessary to accept lvalue factories, and
then take the suggestion to make the test pass.
Replace `auto(*this)` with `_func_impl(*this)`.
This ports a constraint I put on my implementation of
`exec::queries<...>` to the existing one; it requires that a type passed
to `exec::queries` be a possibly-`noexcept` callable that can be invoked
on an archetypal environmnet type with a member `query`.
* Delete unused includes
* Replace `std::invoke_result_t` with `STDEXEC::__invoke_result_t` and
  update the includes
* Replace `std::move` and `std::forward` with the appropriate
  `static_cast`
This diff adds two pointers' worth of storage space to `function` to add
support for capturing callables other than empty lambdas, such as
pointers to functions and pointers to member functions. As a nice side
effect, trivially-copyable, non-empty lambdas are now also supported,
which means member functions can return instances of `function` that
contain a lambda that captures `this`, like so:
```c++
struct impl {
  function<int()> get_int() const {
    return function<int()>([this] { return just(i_); });
  }

  int i_;
;
```
This diff steals the `get_completion_signatures` implementation from
`any_sender_of`; the rules for the two type-erasing containers are the
same. It'd be nice to share an impl somehow, but this is good enough for
now.
This diff changes the implementation of `function<...>` to ensure that
every specialization of the template always sorts-and-uniques the
signatures in the `completion_signatures` specialization given to the
`_func_impl` base class. This way we both minimize the number of base
class template instantiations, and make it easier to make two function
types that happened to specify their completion signaturs in a different
order are "the same" (mutually assignable, comparable, and
constructible).
I don't know why GCC needs this change, but using in-place `new` to
initialize a member of an anonymous union with the result of a function
call rather than directly initializing the same value in the member
initialization clause from the same function allows GCC to recognize
that initializing the value with a prvalue does not invoke the move
constructor.
Looks like MSVC doesn't like pure-virtual member functions on local
classes so move the local types in `function`'s tests out to namespace
scope.
Change the declarations of `function<...>` to inherit from canonical
specializations of `_func_impl` so that the queries are always sorted
and uniqued.
The MSVC build failure on the previous commit looks like a misplaced
`[[no_unique_address]]`; the CUDA build failures are mysterious to me,
but they appear to be downstream of changing `__any`'s `__box` type to
use in-place new into an uninitialized member of an anonymous union,
which I did to address a GCC build failure. This diff makes the
anonymous union hack GCC-specific, to hopefull make all the compilers
happy at the expense of preprocessor complexity.
@ispeters ispeters force-pushed the frame_allocator branch from 025628b to 67066b8 Compare May 3, 2026 16:33
@ericniebler
Copy link
Copy Markdown
Collaborator

/ok to test 67066b8

@ispeters
Copy link
Copy Markdown
Contributor Author

ispeters commented May 3, 2026

The test failure on GCC 14 Debug with ASAN enabled is going to be tricky; GCC doesn't support ASAN on Apple silicon, which is my only dev environment.

@ispeters
Copy link
Copy Markdown
Contributor Author

ispeters commented May 3, 2026

Unfortunately for me, the (gcc 14, Debug, ASAN) failure doesn't repro with Homebrew Clang 22.1.4, Debug, ASAN.

The failures are in test_function.cpp here:

func2_t f4(45, ex::just);
func3_t f5(45, ex::just);

REQUIRE(f4 == f5);

and here:
func1_t f3(45, ex::just);
func2_t f4(45, ex::just);

REQUIRE(f3 == f4);

I'm wondering if GCC's ASAN implementation is mucking with this operator== implementation in some way, like initializing the std::byte make_sender_[sizeof(void *) * 2] member with garbage, or something:

friend constexpr bool operator==(_func_impl const &lhs, _func_impl const &rhs)
noexcept(noexcept(_equal(lhs.args_, rhs.args_)))
{
return lhs.make_op_ == rhs.make_op_
&& std::ranges::equal(lhs.make_sender_, rhs.make_sender_)
&& _equal(lhs.args_, rhs.args_);
}

Comment thread include/exec/function.hpp

// This file defines function<ReturnType(Arguments...)>, which is a
// type-erased sender that can complete with
// - set_value(ReturnType&&)
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i would prefer if it completed with ReturnType instead of ReturnType&&.

Comment thread include/exec/function.hpp
Comment on lines +129 to +143
template <class Env>
constexpr auto choose_frame_allocator(Env const &env) noexcept
{
if constexpr (requires { get_frame_allocator(env); })
{
return get_frame_allocator(env);
}
else if constexpr (requires { get_allocator(env); })
{
return get_allocator(env);
}
else
{
return std::allocator<std::byte>();
}
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

could be:

inline constexpr auto choose_frame_allocator = STDEXEC::__first_callable{
  get_frame_allocator,
  get_allocator,
  STDEXEC::__always{std::allocator<std::byte>()};

__first_callable is handy for these sorts of things.

Comment thread include/exec/function.hpp
Comment on lines +146 to +158
template <class... Args, size_t... I>
bool _equal(std::index_sequence<I...>, __tuple<Args...> const &lhs, __tuple<Args...> const &rhs)
noexcept(noexcept(((__get<I>(lhs) == __get<I>(rhs)) && ...)))
{
return ((__get<I>(lhs) == __get<I>(rhs)) && ...);
}

template <class... Args>
bool _equal(__tuple<Args...> const &lhs, __tuple<Args...> const &rhs)
noexcept(noexcept(_equal(std::index_sequence_for<Args...>{}, lhs, rhs)))
{
return _equal(std::index_sequence_for<Args...>{}, lhs, rhs);
}
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe we should just add operator==(__tuple const&, __tuple const&) = default; to the __tuple structs.

Comment thread include/exec/function.hpp
std::allocator_arg,
alloc,
STDEXEC::connect,
std::invoke(make_sender, static_cast<Args &&>(args)...),
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

prefer STDEXEC::__invoke over std::invoke. it compiles 2x faster. (also use __invocabable above.)

Comment thread include/exec/function.hpp
Comment on lines +341 to +342
template <>
struct _canonical_fn<queries<>>
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i think the specialization above should handle this case as well.

Comment thread include/exec/function.hpp
Comment on lines +401 to +403
using base = _func::_func_impl<STDEXEC::sender_tag(Args...),
_func::_sigs_from_t<Return, false>,
queries<>>;
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is base a part of function's public-facing interface?

Comment thread include/exec/function.hpp
_func::_sigs_from_t<Return, false>,
queries<>>;

using base::base;
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i think everything from here down can be removed. the defaults are correct.

Comment thread include/exec/function.hpp
namespace std
{
template <class... FuncArgs, template <class> class TQual, template <class> class UQual>
struct basic_common_reference<exec::function<FuncArgs...>,
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why is this needed?

Copy link
Copy Markdown
Collaborator

@ericniebler ericniebler left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

comments inline..

Comment thread include/exec/function.hpp
&& (STDEXEC_IS_TRIVIALLY_COPYABLE(Factory)) //
&& (sizeof(Factory) <= sizeof(make_sender_)) //
&& STDEXEC::sender_to<STDEXEC::__invoke_result_t<Factory, Args...>, _receiver_t>
constexpr explicit _func_impl(Args &&...args, Factory factory)
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i would expect the factory to be the first parameter, followed by the arguments.

@ericniebler
Copy link
Copy Markdown
Collaborator

I'm wondering if GCC's ASAN implementation is mucking with this operator== implementation in some way, like initializing the std::byte make_sender_[sizeof(void *) * 2] member with garbage, or something:

i think you're accidentally comparing padding bits for equality. ex::just is 1 byte, but its value could be anything from 0x00 to 0xFF since the type is empty.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants