Summary
stdexec::task_scheduler fails to compile when the type-erased backend takes the heap-allocation fallback path: __any_allocator<std::byte> has a converting constructor that reads the private member of another specialization of the same class template:
template <class _Uy>
constexpr __any_allocator(__any_allocator<_Uy> __other) noexcept
: __alloc_(std::move(__other.__alloc_)) // cross-specialization private access
{ }
That access is rejected by all major compilers — not just MSVC. Verified locally:
- MSVC cl 19.51.36252:
C2248: cannot access private member
- Clang (clang-cl 22.1.8):
error: '__alloc_' is a private member of 'stdexec::__any_allocator<std::byte>'
- GCC 13.3:
error: ... is private within this context
Why upstream never noticed
The converting constructor is a member function template, so it is only instantiated when used. In the upstream test matrix every scheduler's operation state fits in task_scheduler's inline storage, so the backend never falls back to the heap and the constructor is never instantiated. It is instantiated when the operation state is large — e.g. stdexec::task on an asio-based scheduler (asio::post + use_sender, whose opstate exceeds the inline storage) — which breaks the build on every compiler, not just MSVC.
Minimal repro — language level
#include <utility>
template <class T>
struct A {
template <class U>
A(A<U> other) noexcept : x_(std::move(other.x_)) {} // reads other.x_ (private)
A() = default;
private:
T x_;
};
int main() {
A<int> a;
A<long> b(a); // rejected by MSVC, Clang and GCC
}
stdexec repro
#include <stdexec/execution.hpp>
#include <exec/asio/asio_config.hpp>
#include <exec/asio/use_sender.hpp>
#include <asio/io_context.hpp>
struct Sched {
using scheduler_concept = stdexec::scheduler_tag;
asio::io_context* ioc_ = nullptr;
stdexec::sender auto schedule() const noexcept {
return exec::asio::asio_impl::post(*ioc_, exec::asio::use_sender)
| stdexec::upon_error([](std::exception_ptr) noexcept {})
| stdexec::upon_stopped([]() noexcept {});
}
bool operator==(const Sched&) const noexcept = default;
};
int main() {
asio::io_context ioc;
Sched s{&ioc};
stdexec::task_scheduler ts(s, std::allocator<Sched>{});
(void)ts;
}
__any_allocator.hpp(83): error C2248: "stdexec::__any_allocator<std::byte>::__alloc_":
cannot access private member (declared in "stdexec::__any_allocator<std::byte>")
Notes
- Wrapping
inline_scheduler, run_loop::scheduler, or exec::asio::asio_thread_pool's scheduler compiles fine (small operation states, inline storage, converting constructor never instantiated). Only schedulers whose schedule operation state exceeds the inline storage (e.g. asio post + use_sender) hit it.
- Since the constructor is instantiated only on that path, a plain minimal project may or may not reproduce depending on the include set and exact opstate size (
STDEXEC_TASK_SCHEDULE_OPSTATE_SIZE).
Suggested fix
Add an explicit friend declaration to __any_allocator (legal on every compiler):
// in struct __any_allocator, next to the default constructor:
template <class>
friend struct __any_allocator;
See PR #2159 for the implementation and a regression test.
Summary
stdexec::task_schedulerfails to compile when the type-erased backend takes the heap-allocation fallback path:__any_allocator<std::byte>has a converting constructor that reads the private member of another specialization of the same class template:That access is rejected by all major compilers — not just MSVC. Verified locally:
C2248: cannot access private membererror: '__alloc_' is a private member of 'stdexec::__any_allocator<std::byte>'error: ... is private within this contextWhy upstream never noticed
The converting constructor is a member function template, so it is only instantiated when used. In the upstream test matrix every scheduler's operation state fits in
task_scheduler's inline storage, so the backend never falls back to the heap and the constructor is never instantiated. It is instantiated when the operation state is large — e.g.stdexec::taskon an asio-based scheduler (asio::post+use_sender, whose opstate exceeds the inline storage) — which breaks the build on every compiler, not just MSVC.Minimal repro — language level
stdexec repro
Notes
inline_scheduler,run_loop::scheduler, orexec::asio::asio_thread_pool's scheduler compiles fine (small operation states, inline storage, converting constructor never instantiated). Only schedulers whose schedule operation state exceeds the inline storage (e.g. asiopost+use_sender) hit it.STDEXEC_TASK_SCHEDULE_OPSTATE_SIZE).Suggested fix
Add an explicit friend declaration to
__any_allocator(legal on every compiler):See PR #2159 for the implementation and a regression test.