Skip to content

Recursive constraint satisfaction when constructing resource_ref from shared_resource-derived type with resource_ref constructor #8037

@bdice

Description

@bdice

Description

Constructing a resource_ref from a type that:

  1. Publicly inherits from cuda::mr::shared_resource<Impl>, AND
  2. Has a constructor accepting resource_ref

triggers a recursive constraint satisfaction error on GCC 14.3.0 with C++20:

error: satisfaction of atomic constraint '...' depends on itself

The same code compiles if:

  • The derived type does NOT have a resource_ref constructor parameter
  • The derived type is cast to its shared_resource<Impl> base before constructing resource_ref

The Cycle

resource_ref{derived&} checks __satisfies<derived, ...>
  -> __icopyable checks copyable<derived>
    -> is_constructible<derived, derived&> considers derived(resource_ref)
      -> checks if derived& converts to resource_ref
        -> checks __satisfies<derived, ...>  [CYCLE]

GCC evaluates the __icopyable interface for derived by checking copyable<derived>, which checks is_constructible<derived, derived&>. During overload resolution, GCC considers whether derived& can implicitly convert to resource_ref (via the derived(resource_ref) constructor), which requires checking __satisfies<derived, ...> — creating a cycle.

This is the same __satisfies that initiated the check, causing the recursive constraint error.

Minimal Reproducer

https://godbolt.org/z/ddfn1sE8Y (nvcc 13.2, --std=c++20 -arch=sm_75)

#include <cuda/memory_resource>
#include <cuda/stream_ref>
#include <cuda/std/cstddef>

struct my_resource_impl {
  void* allocate(cuda::stream_ref, ::cuda::std::size_t, ::cuda::std::size_t) { return nullptr; }
  void deallocate(cuda::stream_ref, void*, ::cuda::std::size_t, ::cuda::std::size_t) noexcept {}
  void* allocate_sync(::cuda::std::size_t, ::cuda::std::size_t) { return nullptr; }
  void deallocate_sync(void*, ::cuda::std::size_t, ::cuda::std::size_t) noexcept {}
  bool operator==(my_resource_impl const&) const { return true; }
  bool operator!=(my_resource_impl const&) const { return false; }
  friend void get_property(my_resource_impl const&, cuda::mr::device_accessible) noexcept {}
};

using resource_ref = cuda::mr::resource_ref<cuda::mr::device_accessible>;

// A type that inherits from shared_resource AND has a constructor taking resource_ref.
struct derived_resource : public cuda::mr::shared_resource<my_resource_impl> {
  using shared_base = cuda::mr::shared_resource<my_resource_impl>;

  explicit derived_resource(resource_ref /*upstream*/)
    : shared_base(cuda::std::in_place_type<my_resource_impl>) {}

  friend void get_property(derived_resource const&, cuda::mr::device_accessible) noexcept {}
};

void test_fails() {
  auto base_sr = cuda::mr::make_shared_resource<my_resource_impl>();
  resource_ref ref{base_sr};
  derived_resource dr{ref};
  // ERROR: satisfaction of atomic constraint '...' depends on itself
  ref = dr;
}

void test_workaround() {
  auto base_sr = cuda::mr::make_shared_resource<my_resource_impl>();
  resource_ref ref{base_sr};
  derived_resource dr{ref};
  // OK: casting to shared_resource base avoids the cycle
  ref = static_cast<derived_resource::shared_base&>(dr);
}

int main() { return 0; }

Context

This pattern (inheriting from shared_resource<Impl> and accepting resource_ref as an upstream resource) is used extensively in RMM for pool/arena memory resources and adaptor resources. We are currently working around this by casting to the shared_resource<Impl> base before constructing resource_ref, but this is fragile and affects user-facing API ergonomics.

Environment

  • CCCL version: 3.3.0
  • GCC version: 14.3.0
  • C++ standard: C++20
  • CUDA Toolkit: 13.1

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

Status

Done

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions