From 337ef9750291dea2b639f52c49af3c716b7f065e Mon Sep 17 00:00:00 2001 From: deretame Date: Sat, 8 Aug 2026 09:29:16 +0800 Subject: [PATCH 1/3] fix(__any_allocator): add friend declaration so the cross-specialization converting constructor compiles on MSVC __any_allocator's converting constructor reads the private member of another specialization of the same class template, which the standard permits ([class.access], since C++17) but MSVC rejects with C2248. The conversion is instantiated whenever task_scheduler's type-erased backend copies an allocator on the heap-allocation fallback path (e.g. with an asio-based scheduler), so stdexec::task + asio + MSVC currently fails to compile. Add an explicit friend declaration (harmless on GCC/Clang) and a regression test that instantiates the converting constructor directly. Fixes #2158 --- include/stdexec/__detail/__any_allocator.hpp | 7 ++++ test/CMakeLists.txt | 1 + test/stdexec/detail/test_any_allocator.cpp | 37 ++++++++++++++++++++ 3 files changed, 45 insertions(+) create mode 100644 test/stdexec/detail/test_any_allocator.cpp diff --git a/include/stdexec/__detail/__any_allocator.hpp b/include/stdexec/__detail/__any_allocator.hpp index a72bd483a..392798726 100644 --- a/include/stdexec/__detail/__any_allocator.hpp +++ b/include/stdexec/__detail/__any_allocator.hpp @@ -67,6 +67,13 @@ namespace STDEXEC __any_allocator() = default; + // MSVC does not implement [class.access]: members of a class template + // cannot access private members of other specializations of the same + // template. The converting constructor below relies on that rule, so + // declare the friendship explicitly. + template + friend struct __any_allocator; + template <__not_same_as<__any_allocator> _Alloc> requires __is_not_instance_of<_Alloc, __any_allocator> && __simple_allocator<_Alloc> __any_allocator(_Alloc __alloc) noexcept diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index bf974fbc1..31c368410 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -75,6 +75,7 @@ set(stdexec_test_sources stdexec/algos/consumers/test_sync_wait.cpp stdexec/algos/consumers/test_spawn.cpp stdexec/detail/test_any.cpp + stdexec/detail/test_any_allocator.cpp stdexec/detail/test_common_domain.cpp stdexec/detail/test_completion_signatures.cpp stdexec/detail/test_demangle.cpp diff --git a/test/stdexec/detail/test_any_allocator.cpp b/test/stdexec/detail/test_any_allocator.cpp new file mode 100644 index 000000000..0a152e5c4 --- /dev/null +++ b/test/stdexec/detail/test_any_allocator.cpp @@ -0,0 +1,37 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception + * + * Licensed under the Apache License, Version 2.0 with LLVM Exceptions (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://llvm.org/LICENSE.txt + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include + +#include + +#include + +// The converting constructor __any_allocator(_Uy) reads the private member of +// another specialization of the same class template. This is legal since C++17 +// ([class.access]) but MSVC rejects it with C2248; the friend declaration on +// __any_allocator makes the conversion compile everywhere. The conversion is +// instantiated whenever task_scheduler's type-erased backend needs to copy an +// allocator on the heap-allocation fallback path (e.g. with an asio-based +// scheduler), so this test guards the fix for NVIDIA/stdexec#2158. +TEST_CASE("__any_allocator cross-specialization converting constructor compiles", + "[detail][allocator]") +{ + STDEXEC::__any_allocator src; + STDEXEC::__any_allocator dst(std::move(src)); // instantiates the converting ctor + CHECK(dst.has_value() == false); +} From 4ef9b0693de5f5aea940dd433cfc9ec7eaf0bc18 Mon Sep 17 00:00:00 2001 From: deretame Date: Sat, 8 Aug 2026 09:48:59 +0800 Subject: [PATCH 2/3] =?UTF-8?q?docs:=20correct=20the=20rationale=20?= =?UTF-8?q?=E2=80=94=20the=20cross-specialization=20access=20is=20rejected?= =?UTF-8?q?=20by=20GCC,=20Clang=20and=20MSVC=20alike?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The converting constructor of __any_allocator reads the private member of another specialization of the same template. That is rejected by all major compilers (MSVC C2248; Clang/GCC 'private member' error); upstream never noticed because the constructor is only instantiated on the heap-allocation fallback path of task_scheduler (large operation states, e.g. asio-based schedulers), which the upstream test matrix does not exercise. The friend declaration makes the conversion legal on every compiler. --- include/stdexec/__detail/__any_allocator.hpp | 7 +++---- test/stdexec/detail/test_any_allocator.cpp | 15 +++++++++------ 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/include/stdexec/__detail/__any_allocator.hpp b/include/stdexec/__detail/__any_allocator.hpp index 392798726..857fd1661 100644 --- a/include/stdexec/__detail/__any_allocator.hpp +++ b/include/stdexec/__detail/__any_allocator.hpp @@ -67,10 +67,9 @@ namespace STDEXEC __any_allocator() = default; - // MSVC does not implement [class.access]: members of a class template - // cannot access private members of other specializations of the same - // template. The converting constructor below relies on that rule, so - // declare the friendship explicitly. + // The converting constructor below accesses the private member of another + // specialization of this template, which is rejected by MSVC, Clang and + // GCC. Declare the friendship explicitly. template friend struct __any_allocator; diff --git a/test/stdexec/detail/test_any_allocator.cpp b/test/stdexec/detail/test_any_allocator.cpp index 0a152e5c4..3a356529e 100644 --- a/test/stdexec/detail/test_any_allocator.cpp +++ b/test/stdexec/detail/test_any_allocator.cpp @@ -22,12 +22,15 @@ #include // The converting constructor __any_allocator(_Uy) reads the private member of -// another specialization of the same class template. This is legal since C++17 -// ([class.access]) but MSVC rejects it with C2248; the friend declaration on -// __any_allocator makes the conversion compile everywhere. The conversion is -// instantiated whenever task_scheduler's type-erased backend needs to copy an -// allocator on the heap-allocation fallback path (e.g. with an asio-based -// scheduler), so this test guards the fix for NVIDIA/stdexec#2158. +// another specialization of the same class template. That access is rejected by +// all major compilers (MSVC C2248, Clang and GCC report it as accessing a +// private member), so the constructor only compiles because it is never +// instantiated in the upstream test matrix. It *is* instantiated whenever +// task_scheduler's type-erased backend copies an allocator on the +// heap-allocation fallback path (e.g. with an asio-based scheduler), breaking +// the build on every compiler; the friend declaration makes the conversion +// legal. This test instantiates the constructor directly and guards the fix +// for NVIDIA/stdexec#2158. TEST_CASE("__any_allocator cross-specialization converting constructor compiles", "[detail][allocator]") { From 9f40f64663b862b0bf3d8d59bab2841b3ce049ae Mon Sep 17 00:00:00 2001 From: deretame Date: Sun, 9 Aug 2026 10:47:28 +0800 Subject: [PATCH 3/3] style: fix clang-format violations in test_any_allocator.cpp --- test/stdexec/detail/test_any_allocator.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/stdexec/detail/test_any_allocator.cpp b/test/stdexec/detail/test_any_allocator.cpp index 3a356529e..7aa2f4612 100644 --- a/test/stdexec/detail/test_any_allocator.cpp +++ b/test/stdexec/detail/test_any_allocator.cpp @@ -34,7 +34,7 @@ TEST_CASE("__any_allocator cross-specialization converting constructor compiles", "[detail][allocator]") { - STDEXEC::__any_allocator src; - STDEXEC::__any_allocator dst(std::move(src)); // instantiates the converting ctor + STDEXEC::__any_allocator src; + STDEXEC::__any_allocator dst(std::move(src)); // instantiates the converting ctor CHECK(dst.has_value() == false); }