Skip to content

task_scheduler fails to compile: __any_allocator cross-specialization converting constructor accesses a private member #2158

Description

@deretame

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions