| Document #: | P4311R1 |
| Date: | 2026-08-13 |
| Project: | Programming Language C++ LEWG |
| Reply-to: |
Mark Hoemmen <mhoemmen@nvidia.com> Rob Parolin <rparolin@nvidia.com> |
__cpp_lib_mdspan feature test macroMark Hoemmen (NVIDIA)
Rob Parolin (NVIDIA)
Revision 0 to be submitted by 2026-07-15
Revision 1 to be submitted by 2026-08-15
If the nested accessor’s reference type is T&
for some T, then have as_const_accessor just
add const (making the resulting reference type
const T&) instead of using a proxy reference
Expand the argument canonicalization example
Update implementation link and show full implementation in paper
Given an accessor a of type A, a “const
element type version of a” (if it exists) is an accessor
b of a possibly different type B, where
B::data_handle_type is constructible from
A::data_handle_type;
B::reference only permits read access to the element
to which it refers; and
if a_dh is A::data_handle_type and
b_dh is B::data_handle_type(a_dh), then
b and b_dh have the same accessible
range as a and a_dh, and
b.access(b_dh, k) and a.access(a_dh, k)
access the same element for all k in the accessible range
of a_dh.
For example, a const element type version of any
default_accessor<T> instance is any
default_accessor<const T> instance, and a const
element type version of any
aligned_accessor<T, ByteAlignment> instance is any
aligned_accessor<const T, ByteAlignment> instance. We
say “a const element type version” and not “the const element type
version” because it is never unique; an unbounded set of custom accessor
types would satisfy these criteria.
Given an mdspan x whose accessor is
a of type A, and assuming that b
of type B is a const element type version of
a, a “const element type version of x” is an
mdspan
whose data handle is
B::data_handle_type(x.data_handle()),
whose layout mapping is x.mapping(), and
whose accessor is b.
The Standard doesn’t currently have a way to take an arbitrary
accessor and get a const element type version of it. This hinders
development of generic mdspan algorithm libraries.
We propose adding three new features to the C++ Standard Library.
A customization point object (CPO) as_const_access.
It takes an accessor with element type element_type, and
returns a const element type version of the accessor, which it computes
using the rules below.
A new accessor as_const_accessor that wraps an
existing accessor and has const element_type. It behaves
like the mdspan analog of
ranges::as_const_view.
In the special case where the input accessor’s
reference type is T& for some
T, then as_const_accessor just uses
const T& as its reference type.
Otherwise, as_const_accessor’s
reference type wraps the existing accessor’s
reference but only permits reads, not writes.
A function template as_const_mdspan that takes an
mdspan and uses (1) to return a const element type version
of the mdspan.
The CPO computes the const element type version of an accessor using the following rules.
For accessor types whose element_type is already
const, it just returns a copy of the input accessor.
If the accessor has a (possibly static)
as_const_access member, the CPO returns the result of
calling that.
Otherwise, the CPO returns the result of wrapping the input
accessor in as_const_accessor.
For the two existing Standard accessor types that permit nonconst
element_type, default_accessor<T> and
aligned_accessor<T, ByteAlignment>, we propose adding
public static as_const_access members that return
default_accessor<const T> and
aligned_accessor<const T, ByteAlignment>,
respectively.
Suppose we want to write a library of generic mdspan algorithms. The
[linalg] library is an example. We might have some complicated algorithm
my_algorithm that takes a rank-3 mdspan as input and a
rank-2 mdspan as output.
template<
class InElementType, class InExtents, class InLayout, class InAccessor,
class OutElementType, class OutExtents, class OutLayout, class OutAccessor
>
requires(
InExtents::rank() == 3
&& OutExtents::rank() == 2
/* && other constraints ... */
)
void my_algorithm(
mdspan<InElementType, InExtents, InLayout, InAccessor> input,
mdspan<OutElementType, OutExtents, OutLayout, OutAccessor> output)
requires(
decltype(output)::mapping_type::is_always_unique()
/* && other constraints ... */
{
// ... very long, complicated algorithm ...
}Note that we can’t just declare or constrain input to
have const element type. This is because users might call this algorithm
with input that has nonconst element type. We don’t propose
fixing that here; it’s just how argument matching works in C++. The
Standard algorithms have the same issue with their range types; this is
why their input ranges are not constrained on being read-only.
The problem is that if users call this algorithm with
input that has nonconst element type, and later call the
algorithm with input that has const element type,
my_algorithm will be instantiated twice. The Standard
algorithms that take iterators or ranges already have this problem. For
example, if users call ranges::transform with input range
span<float>, and then call it again with input range
span<const float>, then
ranges::transform will be instantiated twice.
Multidimensional algorithms make this problem worse because they have
combinatorially more possibilities of equivalent input resulting in
different instantiations. For instance, even if we don’t care about
optimizing for static extents and are perfectly happy with
InExtents being dims<3>, the compiler
will still instantiate different copies of the algorithm for
InExtents being
extents<size_t, dynamic_extent, 11, 13>,
extents<size_t, 7, dynamic_extent, 13>,
extents<unsigned, 7, 11, dynamic_extent>, and so on.
If we just declare or constrain InExtents to be
dims<3>, then the above algorithm won’t work with
other extents types that are convertible to
dims<3>.
We solve this problem with a process we call argument
canonicalization. We start with the outer, fully generic interface
of the algorithm. Inside that, we “canonicalize” the mdspan
arguments to the types that we want, for example by making their
extents just dims<3>. Then, we call the
implementation of the algorithm. The effect of this process is to reduce
the number of instantiations of the actual algorithm. Our experience is
that if the algorithm is sufficiently complicated, then this reduces
code size and compilation cost. It also helps separate constraint and
error checking from the actual algorithm. Reducing the number of
instantiations also makes techniques like explicit instantiation more
effective.
The my_algorithm example below elaborates the above to
show the uses of argument canonicalization for parameter types besides
just mdspan specializations.
template<
class ScalarType,
class InElementType, class InExtents, class InLayout, class InAccessor,
class OutElementType, class OutExtents, class OutLayout, class OutAccessor
>
requires(
InExtents::rank() == 3
&& OutExtents::rank() == 2
/* && other constraints ... */
)
void my_algorithm(
ScalarType scalar,
mdspan<InElementType, InExtents, InLayout, InAccessor> input,
mdspan<OutElementType, OutExtents, OutLayout, OutAccessor> output)
requires(
decltype(output)::mapping_type::is_always_unique()
/* && other constraints ... */
{
// This will also enforce Mandates and check Preconditions on arguments.
auto [s, in, out] =
impl::canonicalize_arguments_for_my_algorithm(scalar, input, output);
// The algorithm actually runs here.
// It is not exposed to users, so it doesn't need to
// declare constraints or check arguments.
impl::canonical_my_algorithm(s, in, out);
}
namespace impl {
// layout_left -> layout_left_padded<dynamic_extent>,
// layout_right -> layout_right_padded<dynamic_extent>,
// layout_left_padded<P> -> layout_left_padded<dynamic_extent>,
// layout_right_padded<P> -> layout_right_padded<dynamic_extent>,
// everything else passes through.
//
// This models optimizations that attempt to call a C or Fortran
// BLAS _GEMM, that effectively expects one of the padded layouts.
template<class IndexType, size_t ... Extents>
constexpr std::layout_left_padded<std::dynamic_extent>::template mapping<
std::dims<sizeof...(Extents)>
>
canonicalize_layout_mapping(
const std::layout_left::template mapping<
std::extents<IndexType, Extents...>>& map)
{
return map; // implicit conversion
}
template<class IndexType, size_t ... Extents>
constexpr std::layout_right_padded<std::dynamic_extent>::template mapping<
std::dims<sizeof...(Extents)>
>
canonicalize_layout_mapping(
const std::layout_right::template mapping<
std::extents<IndexType, Extents...>>& map)
{
return map; // implicit conversion
}
template<size_t P, class IndexType, size_t ... Extents>
constexpr std::layout_left_padded<std::dynamic_extent>::template mapping<
std::dims<sizeof...(Extents)>
>
canonicalize_layout_mapping(
const std::layout_left_padded<P>::template
mapping<std::extents<IndexType, Extents...>>& map)
{
return map; // implicit conversion
}
template<size_t P, class IndexType, size_t ... Extents>
constexpr std::layout_right_padded<std::dynamic_extent>::template mapping<
std::dims<sizeof...(Extents)>
>
canonicalize_layout_mapping(
const std::layout_right_padded<P>::template mapping<
std::extents<IndexType, Extents...>>& map)
{
return map; // implicit conversion
}
template<class Mapping>
constexpr Mapping canonicalize_layout_mapping(const Mapping& map) { return map; }
constexpr void
assert_extents_compatibility(
[[maybe_unused]] const std::dims<3>& in,
[[maybe_unused]] const std::dims<2>& out)
{
// Check whatever the algorithm requires.
assert(in.extent(1) * in.extent(2) == out.extent(0));
}
template<
class ScalarType,
class InElementType, class InExtents, class InLayout, class InAccessor,
class OutElementType, class OutExtents, class OutLayout, class OutAccessor
>
constexpr auto canonicalize_arguments_for_my_algorithm(
ScalarType scalar,
mdspan<InElementType, InExtents, InLayout, InAccessor> input,
mdspan<OutElementType, OutExtents, OutLayout, OutAccessor> output
)
{
auto map_in = canonicalize_layout_mapping(input.mapping());
auto map_out = canonicalize_layout_mapping(output.mapping());
assert_extents_compatibility(map_in.extents(), map_out.extents());
// It might make sense to convert scalar to a different type, depending
// on the types of the other arguments. For example, it's a common case
// for users of GEMM (the BLAS' matrix-matrix multiply) to supply integers
// like 0 or 1 for scaling factors ALPHA or BETA. If the input and output
// arrays' value types are both double, then we can safely convert the
// scaling factor to double. This may avoid an extra instantiation.
auto canonical_scalar = [&] () {
if constexpr (std::is_same_v<decltype(input)::value_type, double> &&
std::is_same_v<decltype(output)::value_type, double> &&
std::is_integral_v<std::remove_cvref_t<ScalarType>> &&
sizeof(ScalarType) <= sizeof(int))
{
return double(scalar);
}
else {
return scalar;
}
};
// This paper proposes as_const_mdspan.
auto canonical_input =
std::as_const_mdspan(mdspan(input.data_handle(), map_in, input.accessor()));
auto canonical_output = mdspan(output.data_handle(), map_out, output.accessor());
return std::tuple(scalar, canonical_input, canonical_output);
}
// ... define canonicalize_output_for_my_algorithm analogously ...
template<
class InValueType, class InLayout, class InAccessor,
class OutValueType, class OutLayout, class OutAccessor
>
void canonical_my_algorithm(
mdspan<const InValueType, dims<3>, InLayout, InAccessor> input,
mdspan<OutValueType, dims<2>, OutLayout, OutAccessor> output)
{
// ... long, complicated algorithm ...
}
} // namespace implIn the above example, input mdspan canonicalization has
three steps.
Convert the input mapping with generic InExtents to
a possibly less specific input mapping with
dims<3>.
Convert the input accessor to an accessor with const
element_type.
Convert the input data handle to the new accessor’s
data_handle_type.
We know how to do (1). If a layout mapping is well-formed for one
extents specialization, it’s required to work for all extents
specializations of the same rank. That is, if
Layout::mapping<Extents0> is well-formed and
Extents1 is an extents specialization with
Extents1::rank() equal to Extents0::rank(),
then Layout::mapping<Extents1> is well-formed. The
Standard doesn’t require that
Layout::mapping<Extents1> is constructible from
Layout::mapping<Extents0> (perhaps it should!), but
we can generally assume this for all the Standard Layout mappings. (We
leave guaranteeing that this works for user-defined layout mappings to a
separate proposal.)
We do NOT know how to (2). We know how to do it for the Standard
accessors like default_accessor and
aligned_accessor, but we don’t know how to do it for an
arbitrary user-defined accessor.
This situation has come up in practice for the authors. One author
developed this two-layer scheme for generic algorithms in the
kokkos-kernels subproject of the Kokkos project. The scheme
works there because Kokkos’ analog of mdspan,
Kokkos::View, has a way to get a const element type version
of a Kokkos::View. The same author consulted on how to
implement the analogous two-layer scheme for an
mdspan-based generic algorithms library, RAPIDS RAFT. It didn’t work
because there is no way currently to get a const element type version of
an mdspan.
We know how to get const element type versions of the Standard
accessors default_accessor and
aligned_accessor. Users might extrapolate from these by
assuming that all accessors have the form
Accessor<ElementType, Args...>, where the first
template argument is the element type, and the subsequent template
arguments determine the accessor’s behavior. If that assumption is true,
then the const element type version of that accessor would just be
Accessor<const ElementType, Args...>. This assumption
is in fact false, for the following reasons.
const element_type as its
first template argument. (Accessors don’t have to be “const-able.”)Args... be decoupled from the
first template parameter, in such as way that changing
ElementType to const ElementType would give
the const element type version of the accessor.In many cases, users know what a const element type version of their accessor would be. However, the Standard doesn’t give them a way to tell generic code how to get this.
Add a customization point object (CPO)
std::as_const_access. It takes an accessor with element
type element_type, and returns a new accessor with element
type std::add_const_t<element_type>, and with a
reference type that only permits reads, not writes. The CPO
behaves as follows.
For accessor types whose element_type is already
const, the CPO just returns a copy of its input.
For accessor types that have an as_const_access
member function taking zero arguments, the CPO returns the result of
calling that.
For default_accessor<ET>, add an
as_const_access member function that returns
default_accessor<const ET>().
For aligned_accessor<ET, BA>, add an
as_const_access member function that returns
aligned_accessor<const ET, BA>.
Otherwise, the CPO wraps the accessor in a new accessor type
as_const_accessor that behaves analogously to
ranges::as_const_view.
As a convenience for users, add a function
std::as_const_mdspan that takes an mdspan and
uses std::as_const_access to return an mdspan
with const element type. It has the following definition.
template<class ElementType, class Extents, class Layout, class Accessor>
constexpr auto as_const_mdspan(
mdspan<ElementType, Extents, Layout, Accessor> x)
{
auto new_accessor = ::std::as_const_access(x.accessor());
using new_data_handle_type = typename decltype(new_accessor)::data_handle_type;
return mdspan(new_data_handle_type(x.data_handle()), x.mapping(), new_accessor);
}The problem we have is to name three separate things.
There are five options for disambiguating the CPO from the customization function.
We have the following design desiderata.
Users should not be required to define a customization for their custom accessors.
The mdspan design aims to make defining custom
accessors simple. (That’s why it doesn’t ask users to define custom
iterators, for example.)
The accessor might already have const element type, in which case the CPO can just return its input.
The “fall-back” behavior of a wrapping accessor might be acceptable.
Users should have a public interface for getting a const element type version of an accessor, not just a public interface for getting a const element type version of an mdspan.
Argument canonicalization may result in layout mapping changes as
well as accessor changes. We show this in our my_algorithm
example above. Users shouldn’t have to create intermediate
mdspan for each of these steps.
Users may want to apply multiple transformations to accessors
before creating an mdspan from them. For instance, they might start with
an mdspan with nonconst element type, like
default_accessor<float>, turn it to const, and then
wrap it in another accessor that adds logging functionality.
The CPO and the customization should have the same name.
Only the first three options satisfy Desiderata (1) and (2), and only the first two options satisfy all three Desiderata.
What design precedent does the Standard offer?
Precedent from [exec] takes Option (1). We specifically refer to
P2855R1 (Member customization points for Senders and Receivers), which
was merged into P2300R10 and voted into C++26. P2855 changed
customizations of Sender / Receiver operations such as
set_value from using tag_invoke to just
calling member functions of the same name.
Precedent from Ranges takes Option (2); it distinguishes the CPO from
the function by namespace. For example, the CPO
std::ranges::begin(t) looks for unqualified
begin(t) by ADL. Putting the CPO in the
std::ranges namespace avoids collision with existing
nonmember std::begin overloads. This is a natural solution
for Ranges, because all Ranges features live in the
std::ranges namespace anyway. That resolves any
ambiguities. For example, the range adapter object as_const
lives in the namespace std::ranges::views. Despite “view”
being ambiguous in the Standard (the [views] section defines both
span and mdspan, but only span is
a “view” in the Ranges sense), putting “views” inside
std::ranges makes it clear that the Standard means “the
Ranges definition of view.” However, this precedent doesn’t apply very
well here, because we don’t have a nested namespace for
mdspan features. The mdspan class template and
the Standard accessors like default_accessor all live in
the std namespace. We could create a nested namespace
std::accessors and put the CPO in there, but that would
forever reserve the term “accessor” for mdspan-related
features. Alternately, we could create a nested namespace
std::mdspans::accessors and put the CPO in there, but that
would not be consistent with all the other mdspan features
living in the std namespace.
We know of no precedent for Option (3). The CPO
std::compare_strong_order_fallback(a, b) has an option to
invoke the CPO std::strong_order, but there’s no ADL
happening.
Precedent from [linalg] takes Option (4); it makes the CPO
exposition-only. For example, abs-if-needed is an
exposition-only CPO that attempts to use ADL to find
abs(E). The [linalg] library takes this approach because
the CPO is an implementation detail. Users influence the library’s
behavior by defining ADL-findable functions for their number types, as
they normally would for custom number types. The CPO just “regularizes”
the behavior of std::abs for built-in number types, in
order to simplify the definition of [linalg]’s algorithms. However, this
precedent would violate Desideratum (2). Users have good reasons to want
a const element type version of an accessor directly.
Precedent from [mdspan.sub] takes Option (5); it makes the CPO not
exist at all. The only public interface to get the mapping of the mdspan
returned by submdspan is an unqualified call to
submdspan_mapping. The mdspan library lives in
the std namespace. There’s no nested namespace in which to
hide a CPO. While the definition of submdspan attempts to
find submdspan_mapping via ADL, that happens directly, not
by means of a CPO (at least not a CPO that users can see). This works
because submdspan has no reasonable “fall-back” behavior
for submdspan_mapping. If calling it is well-formed, then
submdspan can create the result mapping. Otherwise, it’s a
custom mapping and submdspan would have no idea what to do
with it, so submdspan is ill-formed. However, this
precedent violates Desideratum (1). We don’t want to force users to
define the function for their custom accessors.
Existing precedent in [mdspan.accessor]
(default_accessor and aligned_accessor) and
[linalg] (scaled_accessor and
conjugated_accessor) names accessors
“FOO_accessor,” where FOO is the
distinguishing feature. It also makes these accessors not
exposition-only. We also want the wrapper accessor to be not
exposition-only because users might like to use it in unit tests, or as
a first-pass implementation of their accessor’s customization.
The wrapper accessor works analogously to the
std::ranges::as_const_view view type. Accessors are neither
views in the sense of Ranges, nor views in the sense of [views]
(including span and mdspan). Nevertheless, the
“as_const” part of the name still applies. The wrapper
takes an existing accessor, and makes it behave “as const,” that is, as
an accessor whose element type is const and that only permits read
access. This suggests the name as_const_accessor.
We want to use as_const in the name rather than just
const, in order to avoid colliding with “constant.” A
reasonable way to interpret constant_accessor would be an
accessor where every element is just a compile-time constant.
template<class ValueType, ValueType Value>
requires(std::is_same_v<std::remove_cvref_t<ValueType>, ValueType>)
struct constant_accessor {
using element_type = std::add_const_t<ValueType>;
using reference = ValueType; // access returns a value, not a reference
using offset_policy = constant_accessor<ValueType, Value>;
struct data_handle_type {};
constexpr reference
access(data_handle_type p, size_t k) const noexcept {
return Value;
}
constexpr typename offset_policy::data_handle_type
offset(data_handle_type p, size_t k) const noexcept {
return {};
}
};The CPO needs a different name from the wrapping accessor, since we have already resolved not to introduce new namespaces.
Precedent in [linalg] is scaled_accessor
vs. scaled, and conjugated_accessor
vs. conjugated. There, the accessor has a name of the form
“ADJECTIVE_accessor,” while the function (not a CPO in this
case) that creates an mdspan with that accessor has the name
“ADJECTIVE.” This precedent would suggest
as_const for the CPO. However, std::as_const
is already taken, and we definitely don’t want to overload that with
different behavior. We also don’t want to introduce another namespace to
permit the same name with different behavior, as with
std::ranges::views::as_const.
We don’t want to overload std::ranges::views::as_const.
mdspan is not a range in the sense of
std::ranges::range, so it cannot be a “view” in the sense
of std::ranges::view. The mdspan authors
deliberately chose not to provide iterators. (R3 of P0009 removed
iterator support. R2 only included begin() and
end() for “contiguous” (what eventually became known as
“exhaustive”) layouts.) Nevertheless, some WG21 members have expressed
an interest in making mdspan a range. If that happens, then
we would need std::ranges::views::as_const to behave
consistently for mdspan. A reasonable way to do that would
be for std::ranges::views::as_const to use the mechanism
proposed by this paper.
Calling the CPO for accessors just “as_const” might also
be confusing, because accessors often have no state. The “view” (see
previous paragraph for why this is in double quotes) is the data handle
or the mdspan holding the data handle, not the
accessor.
The above suggests that we want the CPO to include the words
“as_const,” but don’t want to call it that alone. The
adjectival phrase “as_const_access” disambiguates the CPO
from the accessor’s noun form “FOO_accessor.”
“as_const_access_accessor” would be redundant. That leaves
as_const_access.
mdspan function
namingThe previous section suggests a name including as_const,
but not just as_const.
We don’t want to use names that have “view” in them. This is because
the Standard defines “view” ambiguously. The [views] section of the
Standard includes both span and mdspan, yet of
these, only span is a view in the sense of Ranges. It could
make sense to have a function that works for both span and
mdspan, but it’s not obvious to us what to call that
function. The mdspan proposal P0009 called the
submdspan function subspan in R10, but changed
the name to submdspan in R11, likely because of WG21
feedback (“Renamed subspan to submdspan, as
this only applies to mdspan”). This suggests a WG21
preference for interfaces that keep span and
mdspan separate.
We suggest as_const_mdspan as a reasonable
disambiguation. We only need a function, not a CPO and a class as with
as_const_access and as_const_accessor.
The wrapper accessor should only permit read access. This means that
its access function must return a reference
type that does not permit changing the element. This leads to a few
different cases.
If the original accessor’s element_type is already
const, then we don’t have to wrap it.
If the original accessor’s reference is
element_type& or const element_type&,
then the resulting reference should be
const element_type&.
If the original reference is anything else, then
it’s either a value (e.g., const std::vector<bool>
access) or a proxy reference (e.g., nonconst
std::vector<bool> access). We wrap it in our own
proxy reference (as_const_accessor_reference; please see
the Wording below) in both cases.
If the original reference is a value
remove_cv_t<element_type>, then the original
element_type really should have been const, because access
offers no way to change the element. Thus, we don’t need to optimize for
this case. Wrapping it is harmless.
[mdspan.accessor.reqmts] 4 requires that the original
reference models
common_reference_with<reference&&, element_type&>.
Thus, if element_type is const, the original
reference must be read-only. This means that we only have
to change the original reference if the original
element_type is not const. We do that by wrapping the
original proxy reference in a new proxy reference that stores the
original proxy reference and has a conversion to
std::remove_cv_t<element_type>.
We want to solve this problem: “Given an arbitrary accessor, get the
‘same’ accessor but with const element_type.” We could
instead solve a more general problem: “Given an arbitrary accessor, get
the ‘same’ accessor but with a possibly arbitrarily different
element_type.” We call this “rebinding the
accessor’s element type.” It turns out that we don’t actually want to be
able to solve this more general problem, but thinking about it will help
us understand the design better.
Rebinding the accessor’s element type would have two steps.
Note that rebinding does not necessarily mean creating a new data
handle from the old data handle. We don’t have to try to view the same
elements with a different element_type. For example,
rebinding default_accessor<float> to
default_accessor<int> just means getting a new
accessor with the “same properties” as the old one, not viewing the
original float elements as int elements.
That’s why default_accessor<int> is not constructible
from default_accessor<float>. It’s also why we wrote
“in some way” above; we don’t necessarily imply constructibility or
convertibility. On the other hand, if an accessor B is
constructible (or convertible) from another accessor A,
that strongly suggests that B::data_handle_type should be
constructible (or convertible) from A::data_handle_type.
(This is perhaps a missing requirement in the Standard.)
If we had rebinding and wanted to solve the original problem, we
would also need a way to construct the new accessor’s
data_handle_type from the old accessor’s
data_handle_type. We’ll call that the third step in what
follows.
We know how to rebind the Standard accessors
default_accessor and aligned_accessor. All
these accessors can be represented by any class template
template<class ElementType, class...> Accessor, where
the first template parameter is the element type.
Accessor<ElementType, Args...> becomes
Accessor<std::add_const_t<ElementType>, Args...>Accessor<std::add_const_t<ElementType>, Args...>
is constructible from
Accessor<ElementType, Args...>Accessor<std::add_const_t<ElementType>, Args...>::data_handle_type
is constructible from
Accessor<ElementType, Args...>::data_handle_typeThis process does NOT necessarily work for arbitrary accessors. Nothing currently requires accessors to have this form. Accessors don’t need to have template parameters at all. If they do, the first template parameter doesn’t need to be the element type.
C++ Standard allocators have “value type rebind.” Given any allocator
a_u of type A_U whose value type is
U, and a new value type V, users can rebind
V to a new allocator a_v of type
A_V whose value type is V. This has two
steps.
using A_V = allocator_traits<A_U>::rebind_alloc<V>allocator_traits<A_U>::rebind_alloc<V> is
defined to be A_U::rebind<V>::other if that type
alias is present, otherwise SomeAllocator<V, Args> if
A_U is of the form
SomeAllocator<U, Args>, where Args is
zero or more type arguments. That is, one can always use
allocator_traits to rebind the allocator type.
The Allocator named requirements also require that a rebound
allocator is (both copy and move) constructible from the original
allocator. That is, an object of type
allocator_traits<A_U>::rebind_alloc<V> can
always be constructed from an object of type A_U.
One can say that the C++ Standard’s allocators separate “policy”
(allocator_traits) from the allocator itself. The policy
defines a function from value type to allocator type.
The Standard requires that an allocator can be rebound to any value
type. This makes sense because all allocators produce allocations that
“look the same” to C++ code. As long as the same allocator is used to
allocate and deallocate the memory, C++ code can access it just as if it
were created via ordinary new and delete.
Thus, for any type T, as long as an allocator can allocate
a char array with the same alignment as
alignof(T), it can allocate for T.
Accessors are not required to make sense for every possible element
type. For example, a user might define a “lock-free atomic accessor”
that uses atomic_ref<element_type> as its
reference type. This would only make sense if
atomic_ref<element_type>::is_always_lock_free is
true.
In general, accessors don’t even have to access C++ memory. Accessors
introduce to the C++ Standard the possibility of separate “memory”
spaces (where “memory” is in quotes because actual memory need not be
involved) that are inaccessible to ordinary C++ code, except through the
accessor’s access function. Those separate spaces may have
their own requirements on types, such as trivial copyability or
vectorizability ([simd.general] 2).
This tells us that general rebinding of an arbitrary accessor’s element type might not make sense. Instead, we should focus on the nonconst-to-const case.
Even rebinding const to nonconst might not make sense. This is
because some accessors access in a fundamentally read-only way, so it
only ever makes sense for them to have const element_type.
We showed one example above: a “constant_accessor” where
every element is just a compile-time constant.
Layout mappings were explicitly designed to separate “policy” (the
layout type, like layout_right) from the mapping itself
(e.g.,
layout_right::mapping<extents<int, 3, 5, 7>>).
The policy describes a function from extents type to layout mapping
type.
The Standard layout mappings generally also permit conversion between
mapping types with the same layout type and convertible extents types.
The conversion is implicit only if the corresponding conversion between
extents types would also be implicit. This “rebind” facility is
available for all the Standard layout types, as the
layout_stride example below shows.
using original_extents_type = extents<int, 3, dynamic_extent, 7>;
std::array strides{1, 5, 15};
layout_stride::mapping<original_extents_type> map0(original_extents_type(3, 5, 7), strides);
// Conversion with no preconditions is implicit.
layout_stride::mapping<dims<3, int>> map1 = [&] () { return map0; } ();
// Conversion with preconditions is explicit.
layout_stride::mapping<extents<int, 3, 5, 7>> map2(map0);These two features together form a facility to rebind a layout mapping with a different extents object. The layout policy enables rebinding the mapping type, and the mapping’s constructor from a mapping with a different but convertible extents type enables rebinding the mapping instance.
Note that mappings are not required to have this conversion. They are only required to permit rebinding the type, as long as the mapping type supports an extents type of that rank (see proposed resolution of LWG 4582). It is perhaps a design flaw that the requirements include type rebinding but not conversion.
Accessors don’t separate “policy” type from the actual accessor. One
could imagine a different mdspan design in which the actual
accessor type were a function of an accessor policy type and the element
type. It might look like this.
using original_accessor_type = default_accessor_policy::accessor<float>;
using new_accessor_type = original_accessor_policy::policy_type::accessor<const float>;Layout mappings have a public layout_type type alias to
get back to the layout policy, and that has a public
mapping<Extents> type alias to rebind the mapping
type. A hypothetical accessor design could analogously put a
policy_type alias in every accessor to get back to the
accessor policy, and an accessor<ElementType> alias
in the policy to rebind the accessor type. If we also required all
accessors (not just the Standard ones) to have constructors for
conversion from nonconst to const element type, then that would solve
our problem, at least syntactically.
We could draw an analogy between rebinding a layout mapping into a
new mapping with equal extents of different type, and rebinding an
accessor with nonconst element type into a new accessor with const
element type. The two extents objects
extents<int, 3, dynamic_extent, 7>(3, 5, 7) to
extents<int, 3, 5, 7>() are equal and represent the
same multidimensional index space. Going from the first to the second
adds compile-time restrictions, analogous to going from nonconst to
const element type with accessors.
layout_left::mapping map0(extents<int, 3, dynamic_extent, 7>(3, 5, 7));
// Explicit, because of the precondition that map0.extents().extent(1) == 5.
layout_left::mapping<extents<int, 3, 5, 7>> map1(map0);One way in which this analogy could break down is if changing from a
nonconst to const element_type changes how the accessor
accesses its elements. For example, a nonconst accessor that represents
elements on a networked file system might perform destructive,
not-concurrent-safe accesses even on reads, because it happens to be
more efficient on that platform. It would be reasonable for users to
assume that a const accessor would perform concurrent-safe reads.
It seems like a destructively reading accessor would be a bad design. Iterators are supposed to perform safe concurrent reads of distinct elements, regardless of whether the iterator is const or nonconst. Shouldn’t accessors behave the same way? Shouldn’t syntactically read-only access always be semantically read-only?
This proposal depends on that assumption. That is, it assumes that
for an arbitrary accessor with nonconst element type, it “makes semantic
sense” to rebind the accessor to have const element type (and to make
its reference type syntactically read-only) without
otherwise changing its behavior.
The above discussion suggests that all we need is an ability to rebind an accessor from nonconst element type to const element type.
We do not need the ability to rebind between arbitrary
element types, e.g., from float to
std::string, or from const float to
float.
This Compiler Explorer link has a brief implementation with tests.
One coauthor has an implementation of an earlier draft of this proposal in a branch of the CCCL repository.
We show the Compiler Explorer implementation below.
#include <cassert>
#include <concepts>
#include <type_traits>
#include <mdspan>
namespace detail {
// [mdspan.accessor.reqmts]
template<class A>
concept mdspan_accessor_policy =
std::copyable<A> /* 1.1 */ &&
std::is_nothrow_move_constructible_v<A> /* 1.2 */ &&
std::is_nothrow_move_assignable_v<A> /* 1.3 */ &&
std::is_nothrow_swappable_v<A> /* 1.4 */ &&
requires {
typename A::element_type; // 2
typename A::data_handle_type; // 3
typename A::reference; // 4
typename A::offset_policy; // 5
// We can't check 5.1 directly, because concepts aren't allowed to recurse.
typename A::offset_policy::data_handle_type;
typename A::offset_policy::element_type;
} &&
std::is_object_v<typename A::element_type> /* 2 */ &&
(! std::is_abstract_v<typename A::element_type>) /* 2 */ &&
std::same_as<typename A::offset_policy::element_type,
typename A::element_type> /* 5.3 */ &&
// 3
std::copyable<typename A::data_handle_type> &&
std::is_nothrow_move_constructible_v<typename A::data_handle_type> &&
std::is_nothrow_move_assignable_v<typename A::data_handle_type> &&
std::is_nothrow_swappable_v<typename A::data_handle_type> &&
requires(const A a, typename A::data_handle_type p, std::size_t i) {
{ a.access(p, i) } -> std::same_as<typename A::reference>; // 6
{ a.offset(p, i) } -> std::same_as<typename A::offset_policy::data_handle_type>; // 9
};
template<class A>
concept has_as_const_access = requires(const A& a) {
a.as_const_access();
};
} // namespace detail
namespace std {
namespace exposition_only {
// Exposition-only read-only proxy reference type in as_const_accessor.
template <class NestedElementType, class NestedReferenceType>
class as_const_accessor_reference { // exposition-only
private:
using nested_element_type = NestedElementType;
using nested_reference_type = NestedReferenceType;
nested_reference_type ref_;
public:
using value_type = std::remove_cv_t<nested_element_type>;
constexpr explicit
as_const_accessor_reference(nested_reference_type ref)
noexcept(noexcept(nested_reference_type(ref)))
: ref_(ref)
{}
constexpr operator value_type() const
noexcept(noexcept(value_type(ref_)))
{
return value_type(ref_);
}
};
template <class ElementType, class Reference>
using as_const_accessor_reference_type = // exposition-only
conditional_t<
is_reference_v<Reference>,
add_const_t<remove_reference_t<Reference>>&,
as_const_accessor_reference<ElementType, Reference>
>;
} // namespace exposition_only
// Constraints: NestedAccessor meets the accessor policy requirements
// [mdspan.accessor.reqmts].
template <detail::mdspan_accessor_policy NestedAccessor>
class as_const_accessor {
public:
using data_handle_type = typename NestedAccessor::data_handle_type;
using element_type =
std::add_const_t<typename NestedAccessor::element_type>;
using offset_policy =
as_const_accessor<typename NestedAccessor::offset_policy>;
using reference =
exposition_only::as_const_accessor_reference_type<
typename NestedAccessor::element_type,
typename NestedAccessor::reference>;
constexpr as_const_accessor(NestedAccessor acc) : acc_(acc) {}
template<class OtherNestedAccessor>
requires(std::is_constructible_v<NestedAccessor, OtherNestedAccessor>)
explicit(! std::is_convertible_v<OtherNestedAccessor, NestedAccessor>)
constexpr as_const_accessor(OtherNestedAccessor acc)
noexcept(noexcept(acc_(acc)))
: acc_(acc)
{}
constexpr reference access(data_handle_type handle, std::size_t k) const
noexcept(noexcept(reference(acc_.access(handle, k))))
{
return reference(acc_.access(handle, k));
}
constexpr typename offset_policy::data_handle_type
offset(data_handle_type handle, std::size_t k) const
noexcept(noexcept(
typename offset_policy::data_handle_type(acc_.offset(handle, k))))
{
return typename offset_policy::data_handle_type(acc_.offset(handle, k));
}
private:
NestedAccessor acc_;
};
inline namespace as_const_access_detail {
// We don't use an ADL "poison pill" because the customization point object
// doesn't try to invoke any functions by argument-dependent lookup.
// Type of the customization point object.
struct as_const_access_fn {
// The correct way to implement this would be to add a public as_const_access
// function to default_accessor, aligned_accessor, linalg::conjugated_accessor,
// and linalg::scaled_accessor. Instead, we just overload on the relevant
// accessor types.
//
// GCC 16.1 doesn't yet implement <linalg> so we skip the linalg accessors here.
template<class ElementType>
constexpr auto operator() (
const std::default_accessor<ElementType>) const noexcept
{
return std::default_accessor<std::add_const_t<ElementType>>();
}
template<class ElementType, std::size_t ByteAlignment>
constexpr auto operator() (
const std::aligned_accessor<ElementType, ByteAlignment>) const noexcept
{
return std::aligned_accessor<std::add_const_t<ElementType>, ByteAlignment>();
}
// Generic case
template<detail::mdspan_accessor_policy Accessor>
constexpr auto operator() (const Accessor& accessor) const {
// If the accessor already has const element_type,
// then it's already what we want.
// Trust that its reference type behaves correctly.
if constexpr (std::is_const_v<typename Accessor::element_type>) {
return accessor;
}
else if constexpr (detail::has_as_const_access<Accessor>) {
// Call the accessor's as_const_access function if it exists.
//
// Mandates: result of accessor.as_const_access()
// satisfies the mdspan accessor policy requirements.
static_assert(detail::mdspan_accessor_policy<decltype(accessor.as_const_access())>);
return accessor.as_const_access();
}
else {
// Fallback: wrap the accessor in as_const_accessor.
return as_const_accessor(accessor);
}
}
};
} // inline namespace as_const_access_detail
// Customization point object
inline constexpr as_const_access_detail::as_const_access_fn as_const_access{};
template<class ElementType, class Extents, class Layout, class Accessor>
constexpr auto as_const_mdspan(
mdspan<ElementType, Extents, Layout, Accessor> x)
{
auto new_accessor = ::std::as_const_access(x.accessor());
using new_data_handle_type = typename decltype(new_accessor)::data_handle_type;
return mdspan(new_data_handle_type(x.data_handle()), x.mapping(), new_accessor);
}
} // namespace std
////////////////////////////////////////////////////////////////
// TESTS START HERE
////////////////////////////////////////////////////////////////
namespace test {
// Custom accessor with const element_type, proxy reference,
// and as_const_access member function.
template<class ElementType>
requires(std::is_same_v<std::add_const_t<ElementType>, ElementType>)
class const_proxy_reference_accessor_with_as_const_access {
public:
using element_type = ElementType;
using data_handle_type = element_type*;
using offset_policy = const_proxy_reference_accessor_with_as_const_access;
class reference {
private:
using value_type = std::remove_cv_t<element_type>;
element_type& ref_;
public:
constexpr explicit reference(element_type& ref)
: ref_(ref)
{}
constexpr operator value_type() const
noexcept(noexcept(value_type(ref_)))
{
return value_type(ref_);
}
};
constexpr const_proxy_reference_accessor_with_as_const_access() = default;
template<class OtherElementType>
requires(std::is_convertible_v<OtherElementType(*)[], element_type(*)[]>)
constexpr const_proxy_reference_accessor_with_as_const_access(
const const_proxy_reference_accessor_with_as_const_access<OtherElementType>&) noexcept
{}
constexpr reference access(data_handle_type p, size_t i) const noexcept {
return reference(p[i]);
}
constexpr data_handle_type offset(data_handle_type p, size_t i) const noexcept {
return p + i;
}
static constexpr
const_proxy_reference_accessor_with_as_const_access<element_type>
as_const_access() {
return {};
}
};
// Custom accessor with reference = element_type& and as_const_access
// member function. The latter returns a type that's not just
// nonconst_reference_accessor_with_as_const_access<const element_type>.
template<class ElementType>
requires(std::is_same_v<std::remove_const_t<ElementType>, ElementType>)
class nonconst_reference_accessor_with_as_const_access {
public:
using element_type = ElementType;
using data_handle_type = element_type*;
using reference = element_type&;
using offset_policy = nonconst_reference_accessor_with_as_const_access;
constexpr nonconst_reference_accessor_with_as_const_access() = default;
template<class OtherElementType>
requires(std::is_convertible_v<OtherElementType(*)[], element_type(*)[]>)
constexpr nonconst_reference_accessor_with_as_const_access(
nonconst_reference_accessor_with_as_const_access<OtherElementType>) noexcept
{}
constexpr reference access(data_handle_type p, size_t i) const noexcept {
return reference(p[i]);
}
constexpr data_handle_type offset(data_handle_type p, size_t i) const noexcept {
return p + i;
}
static constexpr
const_proxy_reference_accessor_with_as_const_access<
std::add_const_t<element_type>
>
as_const_access() {
return {};
}
};
// Custom accessor with reference = element_type&.
template<class ElementType>
class reference_accessor_without_as_const_access {
public:
using element_type = ElementType;
using data_handle_type = element_type*;
using reference = element_type&;
using offset_policy = reference_accessor_without_as_const_access;
constexpr reference_accessor_without_as_const_access() = default;
template<class OtherElementType>
requires(std::is_convertible_v<OtherElementType(*)[], element_type(*)[]>)
constexpr reference_accessor_without_as_const_access(
const reference_accessor_without_as_const_access<OtherElementType>&) noexcept
{}
constexpr reference access(data_handle_type p, size_t i) const noexcept {
return p[i];
}
constexpr data_handle_type offset(data_handle_type p, size_t i) const noexcept {
return p + i;
}
};
void test_as_const_access() {
static_assert(detail::mdspan_accessor_policy<std::default_accessor<float>>);
static_assert(
std::is_same_v<
decltype(std::as_const_access(std::default_accessor<float>())),
std::default_accessor<const float>
>);
static_assert(detail::mdspan_accessor_policy<std::default_accessor<const float>>);
static_assert(
std::is_same_v<
decltype(std::as_const_access(std::default_accessor<const float>())),
std::default_accessor<const float>
>);
static_assert(detail::mdspan_accessor_policy<std::aligned_accessor<float, 16>>);
static_assert(
std::is_same_v<
decltype(std::as_const_access(std::aligned_accessor<float, 16>())),
std::aligned_accessor<const float, 16>
>);
static_assert(detail::mdspan_accessor_policy<std::aligned_accessor<const float, 16>>);
static_assert(
std::is_same_v<
decltype(std::as_const_access(std::aligned_accessor<const float, 16>())),
std::aligned_accessor<const float, 16>
>);
static_assert(detail::mdspan_accessor_policy<
test::const_proxy_reference_accessor_with_as_const_access<const float>>);
static_assert(detail::has_as_const_access<
test::const_proxy_reference_accessor_with_as_const_access<const float>>);
static_assert(
std::is_same_v<
decltype(std::as_const_access(
test::const_proxy_reference_accessor_with_as_const_access<const float>())),
test::const_proxy_reference_accessor_with_as_const_access<const float>
>);
static_assert(detail::mdspan_accessor_policy<
test::nonconst_reference_accessor_with_as_const_access<float>>);
static_assert(detail::has_as_const_access<
test::nonconst_reference_accessor_with_as_const_access<float>>);
static_assert(
std::is_same_v<
decltype(std::as_const_access(
test::nonconst_reference_accessor_with_as_const_access<float>())),
test::const_proxy_reference_accessor_with_as_const_access<const float>
>);
static_assert(std::is_same_v<
test::reference_accessor_without_as_const_access<float>::reference,
float&
>);
static_assert(detail::mdspan_accessor_policy<
test::reference_accessor_without_as_const_access<float>>);
static_assert(! detail::has_as_const_access<
test::reference_accessor_without_as_const_access<float>>);
static_assert(
std::is_same_v<
decltype(std::as_const_access(
test::reference_accessor_without_as_const_access<float>())
),
std::as_const_accessor<
test::reference_accessor_without_as_const_access<float>
>
>);
static_assert(
std::is_same_v<
std::as_const_accessor<
test::reference_accessor_without_as_const_access<float>
>::reference,
const float&
>);
static_assert(
std::is_same_v<
decltype(std::as_const_access(
test::reference_accessor_without_as_const_access<float>())
)::reference,
const float&
>);
static_assert(std::is_same_v<
test::reference_accessor_without_as_const_access<const float>::reference,
const float&
>);
static_assert(detail::mdspan_accessor_policy<
test::reference_accessor_without_as_const_access<const float>>);
static_assert(! detail::has_as_const_access<
test::reference_accessor_without_as_const_access<const float>>);
static_assert(
std::is_same_v<
decltype(std::as_const_access(
test::reference_accessor_without_as_const_access<const float>())),
test::reference_accessor_without_as_const_access<const float>
>);
static_assert(
std::is_same_v<
std::as_const_accessor<
test::reference_accessor_without_as_const_access<const float>
>::reference,
const float&
>);
static_assert(
std::is_same_v<
decltype(std::as_const_access(
test::reference_accessor_without_as_const_access<const float>())
)::reference,
const float&
>);
}
template<class Accessor>
void test_as_const_accessor() {
using reference = typename std::as_const_accessor<Accessor>::reference;
static_assert(
std::common_reference_with<reference&&, typename Accessor::element_type&>
);
static_assert(
std::common_reference_with<typename Accessor::element_type&, reference&&>
);
using value_type = std::remove_const_t<typename Accessor::element_type>;
value_type value = value_type(7);
reference ref(value);
value = value_type(8);
value_type value2 = ref;
assert(value == value2);
assert(value == value_type(8));
reference ref2(ref);
reference ref3(ref);
assert(value_type(ref2) == value_type(8));
assert(value_type(ref3) == value_type(8));
value = value_type(9);
assert(value_type(ref) == value_type(9));
assert(value_type(ref2) == value_type(9));
assert(value_type(ref3) == value_type(9));
}
} // namespace test
int main() {
test::test_as_const_access();
test::test_as_const_accessor<test::reference_accessor_without_as_const_access<float>>();
test::test_as_const_accessor<std::default_accessor<float>>();
// as_const_access will never produce an as_const_accessor
// whose nested accessor has const element_type.
// Nevertheless, we need to test that as_const_accessor works
// for accessors with const element type,
// since as_const_accessor is user-accessible and
// users can put any accessor type they like in it.
test::test_as_const_accessor<test::reference_accessor_without_as_const_access<const float>>();
test::test_as_const_accessor<std::default_accessor<const float>>();
return 0;
}Text in blockquotes is not proposed wording, but rather instructions for generating proposed wording.
__cpp_lib_mdspan feature test macroIn [version.syn], increase the value of the
__cpp_lib_mdspanmacro by replacing YYYMML below with the integer literal encoding the appropriate year (YYYY) and month (MM).
#define __cpp_lib_mdspan YYYYMML // also in <mdspan>Change [mdspan.syn] (“Header
synopsis”) as follows.
// [mdspan.layout], layout mapping
struct layout_left;
struct layout_right;
struct layout_stride;
template<size_t PaddingValue = dynamic_extent>
struct layout_left_padded;
template<size_t PaddingValue = dynamic_extent>
struct layout_right_padded;
// [mdspan.accessor.default], class template default_accessor
template<class ElementType>
class default_accessor;
// [mdspan.accessor.aligned], class template aligned_accessor
template<class ElementType, size_t ByteAlignment>
class aligned_accessor;
// [mdspan.accessor.as_const_accessor], class template as_const_accessor
class as_const_accessor;
// [mdspan.accessor.as_const_access], customization point object as_const_access
inline namespace unspecified {
inline constexpr unspecified as_const_access = unspecified;
}
// [mdspan.accessor.as_const_mdspan], function template as_const_mdspan
template<class ElementType, class Extents, class Layout, class Accessor>
constexpr auto as_const_mdspan(mdspan<ElementType, Extents, Layout, Accessor>); // [mdspan.mdspan], class template mdspan
template<class ElementType, class Extents, class LayoutPolicy = layout_right,
class AccessorPolicy = default_accessor<ElementType>>
class mdspan;Change the existing section “[mdspan.accessor.default], Class template
default_accessor” as specified below.
namespace std {
template<class ElementType>
struct default_accessor {
using offset_policy = default_accessor;
using element_type = ElementType;
using reference = ElementType&;
using data_handle_type = ElementType*;
constexpr default_accessor() noexcept = default;
template<class OtherElementType>
constexpr default_accessor(default_accessor<OtherElementType>) noexcept;
constexpr reference access(data_handle_type p, size_t i) const noexcept;
constexpr data_handle_type offset(data_handle_type p, size_t i) const noexcept; static constexpr default_accessor<const element_type> as_const_access() noexcept {
return {};
} };
}Change the existing section “[mdspan.accessor.aligned], Class template
aligned_accessor” as specified below.
namespace std {
template<class ElementType, size_t ByteAlignment>
struct aligned_accessor {
using offset_policy = default_accessor<ElementType>;
using element_type = ElementType;
using reference = ElementType&;
using data_handle_type = ElementType*;
static constexpr size_t byte_alignment = ByteAlignment;
constexpr aligned_accessor() noexcept = default;
template<class OtherElementType, size_t OtherByteAlignment>
constexpr aligned_accessor(
aligned_accessor<OtherElementType, OtherByteAlignment>) noexcept;
template<class OtherElementType>
constexpr explicit aligned_accessor(default_accessor<OtherElementType>) noexcept;
template<class OtherElementType>
constexpr operator default_accessor<OtherElementType>() const noexcept;
constexpr reference access(data_handle_type p, size_t i) const noexcept;
constexpr typename offset_policy::data_handle_type offset(
data_handle_type p, size_t i) const noexcept; static constexpr aligned_accessor<const element_type> as_const_access() noexcept {
return {};
} };
}Add a new section “[mdspan.accessor.as_const_accessor], Class template
as_const_accessor” between [mdspan.accessor.aligned] and [mdspan.mdspan], with the following content.
1
The class template as_const_accessor is an
mdspan accessor policy that wraps an existing
mdspan accessor policy, and provides read-only access to
the elements accessed by the wrapped policy. It is part of the
implementation of as_const_access
([mdspan.accessor.as_const_access]).
template<class ElementType, class Reference>
using as-const-accessor-reference-type = // exposition-only
/* see-below */;
template <class NestedAccessor>
class as_const_accessor {
public:
using data_handle_type = typename NestedAccessor::data_handle_type;
using element_type =
std::add_const_t<typename NestedAccessor::element_type>;
using offset_policy =
as_const_accessor<typename NestedAccessor::offset_policy>;
// [mdspan.accessor.as_const_accessor.ref] Proxy reference type
using reference = as-const-accessor-reference-type<
typename NestedAccessor::element_type,
typename NestedAccessor::reference>;
// [mdspan.accessor.as_const_accessor.cons] Constructors
constexpr as_const_accessor(NestedAccessor acc);
template<class OtherNestedAccessor>
explicit(see below)
constexpr as_const_accessor(OtherNestedAccessor acc)
noexcept(noexcept(acc_(acc)));
// [mdspan.accessor.as_const_accessor.members] Members
constexpr reference
access(data_handle_type handle, std::size_t k) const
noexcept(noexcept(reference(acc_.access(handle, k))));
constexpr typename offset_policy::data_handle_type
offset(data_handle_type handle, std::size_t k) const
noexcept(noexcept(see below));
private:
NestedAccessor acc_; // exposition-only
};2
Constraints: NestedAccessor meets the accessor
policy requirements ([mdspan.accessor.reqmts]).
3
Each specialization as_const_accessor<NA> of
as_const_accessor models copyable
and
(3.1)
is_nothrow_move_constructible_v<as_const_accessor<NA>>
is true if
is_nothrow_move_constructible_v<NA> istrue`,
(3.2)
is_nothrow_move_assignable_v<as_const_accessor<NA>>
is true if
is_nothrow_move_assignable_v<NA> istrue`,
and
(3.3)
is_nothrow_swappable_v<as_const_accessor<NA>>
is true if
is_nothrow_swappable_v<NA> istrue`.
4
Each specialization as_const_accessor<NA> is a
trivially copyable type if NA is a trivially copyable
type.
1
as-const-accessor-reference denotes the following
exposition-only class template.
template<NestedElementType, class NestedReferenceType>
class as-const-accessor-reference { // exposition-only
private:
NestedReferenceType ref_;
public:
using value_type = std::remove_cv_t<NestedElementType>;
constexpr explicit
as_const_accessor_reference(NestedReferenceType ref)
noexcept(noexcept(NestedReferenceType(ref)))
: ref_(ref)
{}
constexpr operator value_type() const
noexcept(noexcept(value_type(ref_)))
{
return value_type(ref_);
}
};2
The exposition-only type alias
as-const-accessor-reference-type<ElementType, Reference>
denotes the type
constexpr as_const_accessor(NestedAccessor acc);1
Effects: Direct-non-list-initializes acc_ with
acc.
template<class OtherNestedAccessor>
explicit(see below)
constexpr as_const_accessor(OtherNestedAccessor acc)
noexcept(noexcept(acc_(acc)));2 Constraints:
(2.1)
OtherNestedAccessor meets the accessor policy requirements
([mdspan.accessor.reqmts]).
(2.2)
NestedAccessor is constructible from
OtherNestedAccessor.
3
Effects: Direct-non-list-initializes acc_ with
acc.
4
Remarks: The expression inside explicit is
equivalent to:
! std::is_convertible_v<OtherNestedAccessor, NestedAccessor>.
constexpr reference access(data_handle_type handle, std::size_t k) const
noexcept(noexcept(reference(acc_.access(handle, k))));1
Returns: reference(acc_.access(handle, k)).
constexpr typename offset_policy::data_handle_type
offset(data_handle_type handle, std::size_t k) const
noexcept(noexcept(see below))
{
return typename offset_policy::data_handle_type(acc_.offset(handle, k));
}2
Returns:
typename offset_policy::data_handle_type(acc_.offset(handle, k)).
3
Remarks: The expression inside the noexcept
specifier is equivalent to:
typename offset_policy::data_handle_type(acc_.offset(handle, k)).
Add a new section “[mdspan.accessor.as_const_access], Customization point object
as_const_access” between [mdspan.accessor.as_const_accessor] and [mdspan.accessor.aligned], with the following content.
1
Given an mdspan accessor policy a of type
A, a “const element type version of a” (if it
exists) is an mdspan accessor b of a possibly
different type B, where
(1.1)
B::data_handle_type is constructible from
A::data_handle_type;
(1.2)
B::reference only permits read access to the element to
which it refers; and
(1.3) if
a_dh is A::data_handle_type and
b_dh is B::data_handle_type(a_dh), then
[Example: A const element type version of any
default_accessor<T> instance is any
default_accessor<const T> instance, and a const
element type version of any
aligned_accessor<T, ByteAlignment> instance is any
aligned_accessor<const T, ByteAlignment> instance.
–end example]
2
Let A be an mdspan accessor policy. If the
expression
as_const(std::declval<A>()).as_const_access() is
well-formed, then the type of that expression is a const element type
version of A, else the program is ill-formed, no diagnostic
required.
[Editorial note: Consider whether checking all of the requirements at compile time is always possible. If so, then we can change IFNDR to simply ill-formed. Also, consider moving the above paragraph to [mdspan.accessor.reqmts]. –end editorial note]
3
The name as_const_access denotes a customization point
object. For a subexpression a, let A denote
decltype(a). The expression as_const_access(a)
is equivalent to the following.
Add a new section “[mdspan.accessor.as_const_mdspan], Function template
as_const_mdspan” between [mdspan.accessor.as_const_access] and [mdspan.accessor.aligned], with the following content.
template<class ElementType, class Extents, class Layout, class Accessor>
constexpr auto as_const_mdspan(mdspan<ElementType, Extents, Layout, Accessor> x);1 Effects: Equivalent to:
using result_accessor_type = decltype(as_const_access(x.accessor()));
using result_data_handle_type = typename result_accessor_type::data_handle_type;
return mdspan(result_data_handle_type(x.data_handle()), x.mapping(), as_const_access(x.accessor()));Thanks to our colleague Sam Li (NVIDIA) for reviewing the proposal!