C++23 std::mdspan and Deducing This
One flat buffer, three views over it, and one member function that serves const and non-const objects. std::mdspan needs GCC 16 on this box, deducing this needs GCC 14, and Clang 18 cannot use either one with the libraries installed here. All of that is shown with the actual compiler output.
One buffer, three views
std::mdspan is a multidimensional array view that maps a multidimensional index to an element of an array, and the mapping and element access policies are configurable. That sentence from the cppreference page for std::mdspan is the whole design: a view, a mapping from indices to offsets, and a handle to the underlying data. The view does not own the data. The mapping decides what m[i, j] means. The rest of this guide builds one small library on those two facts.
The library works on a flat buffer holding a 2-row by 3-column matrix, values 1 through 6, and on a second flat buffer holding a 2-by-2 image with three channels per pixel, values 10 through 120. Three views read the same buffers three ways: row-major, column-major, and strided. The member functions on the wrapper use deducing this, the C++23 explicit object parameter, so one function serves const and non-const objects. The previous guide in this course built error-handling pipelines with std::expected. See C++23 std::expected Error-Handling Pipelines. A fallible view constructor returning std::expected would use that guide's pattern; this guide keeps construction infallible and concentrates on the views.
The first view is the default. With no layout policy named, mdspan uses layout_right, the row-major policy: the rightmost index has stride 1. The constructor takes the data pointer and the extents, and the type is deduced:
std::vector<int> buf{1, 2, 3, 4, 5, 6};
// 1. Row-major view: layout_right, rightmost index stride 1.
std::mdspan right(buf.data(), 2, 3);
std::printf("right[1,2]=%d extent0=%zu extent1=%zu size=%zu stride0=%zu stride1=%zu\n",
right[1, 2], (std::size_t)right.extent(0), (std::size_t)right.extent(1),
(std::size_t)right.size(), (std::size_t)right.stride(0),
(std::size_t)right.stride(1));
The output is:
right[1,2]=6 extent0=2 extent1=3 size=6 stride0=3 stride1=1
right[1, 2] is 6, the last element. The stride along dimension 0 is 3, because moving one row down skips a whole row of three elements. The stride along dimension 1 is 1, because moving one column over moves to the next element. The multidimensional subscript m[i, j] is C++23 syntax: the comma inside the brackets is the multidimensional subscript operator, not the comma operator. The feature-test macro for that syntax is __cpp_multidimensional_subscript, and the guard program in the last section prints its value.
Extents: static, dynamic, and the observers
Extents describe the shape of the index space: how many dimensions, and how large each one is. An extent is static when its size is part of the type, and dynamic when it is supplied at run time. dextents is the all-dynamic form. The first view above used deduced dynamic extents. The same buffer with static extents needs no extent arguments, because the shape is in the type:
std::mdspan<int, std::extents<std::size_t, 2, 3>> fixed(buf.data());
std::printf("fixed[0,1]=%d static_extent0=%zu\n", fixed[0, 1],
(std::size_t)fixed.static_extent(0));
The output is:
fixed[0,1]=2 static_extent0=2
Static extents suit shapes known at compile time, such as a fixed 3-by-3 kernel. Dynamic extents suit shapes read from a file header or an image size, where the program learns the shape at run time. The library in this guide uses dynamic extents for the image views and static extents only for this one demonstration, because the image sizes are data, not constants.
The observers report what the mapping does. rank is the number of dimensions. extent and stride report one dimension at a time. size is the size of the index space. Three more observers classify the mapping: is_unique means every combination of indices maps to a different underlying element, is_exhaustive means every underlying element is reachable through some combination of indices, and is_strided means incrementing an index in any dimension always jumps over the same number of underlying elements. For the row-major view, all three are true:
right: rank=2 is_strided=1 is_exhaustive=1 is_unique=1
A view is not an owner. The buffer must outlive every view over it, and resizing or destroying the buffer while a view exists leaves a dangling view. Nothing in mdspan prevents that: the type is a handle plus a mapping, and it holds no lifetime information. Keep the buffer alive for as long as any view is used, and treat a view returned from a function as borrowed, in the same way as a std::span.
The column-major view and the strided views
The second view uses layout_left, the column-major policy: the leftmost extent has stride 1. Over the same six-element buffer, a layout_left view with extents (3, 2) is the transpose of the row-major view with extents (2, 3). The program checks that directly, comparing every element pair:
std::mdspan<int, std::dextents<std::size_t, 2>, std::layout_left> left(
buf.data(), 3, 2);
bool transpose_ok = true;
for (std::size_t i = 0; i < 2; ++i)
for (std::size_t j = 0; j < 3; ++j)
transpose_ok = transpose_ok && (left[j, i] == right[i, j]);
std::printf("transpose ok=%d left[2,1]=%d stride0=%zu stride1=%zu\n",
(int)transpose_ok, left[2, 1], (std::size_t)left.stride(0),
(std::size_t)left.stride(1));
The output is:
transpose ok=1 left[2,1]=6 stride0=1 stride1=3
The strides are the mirror image of the row-major view: stride 1 along dimension 0, stride 3 along dimension 1. Nothing was copied or reordered. The same six integers are read in a different order because the mapping changed.
The third policy, layout_stride, is a layout mapping policy with user-defined strides. It is the policy that makes mdspan useful for real buffers, because real buffers are rarely one dense matrix. An interleaved image buffer stores pixels as red, green, blue, red, green, blue. A channel view must step by three to stay on one channel. The mapping for that view is built from extents and a stride array, and the view is built from the data pointer and the mapping:
std::vector<int> rgb{10, 20, 30, 40, 50, 60, 70, 80, 90, 100, 110, 120};
using StrideMap1 = std::layout_stride::mapping<std::dextents<std::size_t, 1>>;
StrideMap1 red_map{std::dextents<std::size_t, 1>{4},
std::array<std::size_t, 1>{3}};
std::mdspan<int, std::dextents<std::size_t, 1>, std::layout_stride> red(
rgb.data(), red_map);
std::printf("red: %d %d %d %d is_strided=%d is_exhaustive=%d\n", red[0],
red[1], red[2], red[3], (int)red.is_strided(),
(int)red.is_exhaustive());
The output is:
red: 10 40 70 100 is_strided=1 is_exhaustive=0
The red channel is 10, 40, 70, 100: every third element starting at offset 0. The view is strided, because each step jumps by the same amount, and it is not exhaustive, because the green and blue elements are never reached through this view. The green channel is the same view starting at offset 1, and the blue channel starts at offset 2. The observers say exactly what the view can and cannot reach, which is why the guide prints them instead of asserting them in prose.
The same mechanism crops without copying. A crop is a strided view whose pointer starts at the crop origin and whose strides skip the cropped-out columns and rows. The top-left 2-by-2 block of the 4-by-3 pixel-by-channel matrix, first two channels only, uses strides {3, 1}: step 3 to move one pixel row down, step 1 to move one channel over:
using StrideMap2 = std::layout_stride::mapping<std::dextents<std::size_t, 2>>;
StrideMap2 crop_map{std::dextents<std::size_t, 2>{2, 2},
std::array<std::size_t, 2>{3, 1}};
std::mdspan<int, std::dextents<std::size_t, 2>, std::layout_stride> crop(
rgb.data(), crop_map);
std::printf("crop: %d %d / %d %d\n", crop[0, 0], crop[0, 1], crop[1, 0],
crop[1, 1]);
The output is:
crop: 10 20 / 40 50
No element was moved. The crop is a new mapping over the same twelve integers, and writing through crop writes into the original buffer at the mapped offsets. That is the point of a view library: one buffer, many mappings, no copies.
Deducing this: one member function, both constnesses
Before C++23, a member function that had to work for both const and non-const objects was written twice, or written once with a cast. A class with four reference qualifications, &, const&, &&, and const&&, needed four overloads of the same function. Deducing this, the explicit object parameter from P0847, collapses those overloads into one templated member function whose first parameter is the object itself, written this Self&& self. The function deduces whether the object is const, and whether it is an lvalue or an rvalue, from the call site.
The wrapper class below owns its buffer and offers one at function. The return type is decltype(auto), so the function returns exactly what the buffer access returns: int& for a non-const object, const int& for a const object. The forwarding inside the function preserves the constness and value category of self:
// Non-template base: deducing this lets one member function work for any
// derived view type, without the base being a template (the CRTP shape,
// without the template parameter).
struct ViewOps {
std::string describe(this auto&& self) {
return "rows=" + std::to_string(self.rows()) +
" cols=" + std::to_string(self.cols());
}
};
// A small grid that owns its buffer. The deducing-this member `at` is a
// single function serving both const and non-const objects: the constness
// of `self` decides whether the caller gets `int&` or `const int&`.
struct Grid : ViewOps {
std::vector<int> data;
std::size_t rows_, cols_;
Grid(std::vector<int> d, std::size_t r, std::size_t c)
: data(std::move(d)), rows_(r), cols_(c) {}
std::size_t rows() const { return rows_; }
std::size_t cols() const { return cols_; }
decltype(auto) at(this auto&& self, std::size_t i, std::size_t j) {
return std::forward<decltype(self)>(self).data[i * self.cols_ + j];
}
};
The use site shows both halves. Writing through the non-const object changes the buffer. Reading through a const reference to the same object returns the same values, and the static assertions pin down the return types at compile time:
Grid g({1, 2, 3, 4, 5, 6}, 2, 3);
g.at(1, 2) = 60;
const Grid& cg = g;
std::printf("grid at(1,2)=%d const-at(0,1)=%d describe=%s\n", g.at(1, 2),
cg.at(0, 1), g.describe().c_str());
static_assert(std::is_same_v<decltype(g.at(0, 0)), int&>);
static_assert(std::is_same_v<decltype(cg.at(0, 0)), const int&>);
The output is:
grid at(1,2)=60 const-at(0,1)=2 describe=rows=2 cols=3
The value 60 is the write through the non-const object. The const read returns 2, the original value at that position. If the two static_assert lines are removed, the program still runs the same way; they exist to make the constness claim checkable instead of stated. Note what deducing this does not do here: it does not make a const mdspan propagate constness to its elements. A const mdspan still yields mutable references, because mdspan is a view in the same way std::span is. The wrapper above propagates constness because at indexes the owned vector, not a stored mdspan. Keep the two mechanisms separate in your head: deducing this decides the function from the object's constness, and the member it touches decides what that constness means for the data.
The base class without the template
The ViewOps base in the listing above is the second use of deducing this, and it replaces a pattern that used to need a template. The curiously recurring template pattern, CRTP, writes a base class templated on its derived class so the base can call derived members. The base in this guide is not a template at all. Its describe member takes this auto&& self, and self carries the derived type with it:
struct ViewOps {
std::string describe(this auto&& self) {
return "rows=" + std::to_string(self.rows()) +
" cols=" + std::to_string(self.cols());
}
};
Grid inherits from ViewOps with no template argument, and describe calls self.rows() and self.cols(), which are Grid members. The base never names the derived type. The same shape appears in the ISciNumPy C++23 survey, where a CLI11 options base class stops being a template over its children and takes the derived type through the explicit object parameter instead. The survey also names recursive lambdas as another use: a lambda taking this auto self can call itself by that name, with no std::function and no named type. This guide does not use recursive lambdas; the pattern is recorded here because it is the same mechanism, and a reader meeting it elsewhere should recognize it as deducing this, not as a new feature.
The practical rule is narrow. Use deducing this where one function must serve several constnesses or several derived types and the differences are only in the object itself. Do not use it to make a function generic over unrelated argument types; that is what ordinary templates are for. The at function above is generic over the object and specific about its arguments, which is the shape the feature fits.
What C++23 does not give you
C++23 ships mdspan without a sub-view operation. std::submdspan, which returns a view of a subset of an existing mdspan, is C++26, and so are submdspan_extents, the padded layouts, aligned_accessor, and the bounds-checked at member function. The cppreference page for std::mdspan marks each of those (C++26) in its member and helper tables. Writing std::submdspan under -std=c++23 is therefore a compile error, and the guide shows it failing rather than describing it:
#include <mdspan>
#include <vector>
#include <cstdio>
// Negative test: submdspan is C++26, not C++23. This must NOT compile
// at -std=c++23. Kept as a separate file so its failure is recorded alone.
int main() {
std::vector<int> v{1, 2, 3, 4, 5, 6};
std::mdspan m(v.data(), 2, 3);
auto s = std::submdspan(m, 0, std::full_extent);
std::printf("%d\n", s[0]);
return 0;
}
GCC 16.2.0 rejects it with:
error: 'submdspan' is not a member of 'std'; did you mean 'mdspan'?
The hand-rolled replacement is the strided-view construction from the third section, with the pointer moved to the sub-view origin. This sub-view takes rows 1 through 1 and channels 1 through 2 of the 4-by-3 matrix: the pointer starts at offset 1 * 3 + 1, the extents are 1 by 2, and the strides stay {3, 1}:
StrideMap2 sub_map{std::dextents<std::size_t, 2>{1, 2},
std::array<std::size_t, 2>{3, 1}};
std::mdspan<int, std::dextents<std::size_t, 2>, std::layout_stride> sub(
rgb.data() + 1 * 3 + 1, sub_map);
std::printf("subview: %d %d\n", sub[0, 0], sub[0, 1]);
The output is:
subview: 50 60
Label hand-rolled sub-views as hand-rolled. They are easy to get wrong in exactly the ways submdspan will get right: the offset arithmetic, the stride for a sliced dimension, and the extent after slicing. When a C++26 library is available, prefer submdspan and delete the manual construction. Until then, the manual form is the only form, and the guide keeps it in one place, with the origin offset written as an expression over the row stride, so the arithmetic stays readable.
Compiler matrix and the guard program
The cppreference C++23 compiler-support table, re-opened for this build, gives the floors: std::mdspan in GCC libstdc++ 16, Clang libc++ 17 (partial) and 18, and MSVC 19.39; deducing this in GCC 14, Clang 18, and MSVC 19.32 (partial) and 19.43. The two features therefore have different floors, and the build floor for the combined library is set by mdspan: GCC 16, Clang 18 with libc++, or MSVC 19.39, at -std=c++23. Deducing this alone compiles on GCC 14, as the separate program below proves, but the combined program needs the newer library.
What was actually tested on the build box is narrower, and the table below says so. GCC 15 was not tested: no GCC 15 is installed on the box. MSVC was not tested at all. Clang was tested and failed, for reasons specific to this box and its library pairings.
| Compiler | <mdspan> |
Deducing this |
Result on this box |
|---|---|---|---|
| GCC 16.2.0 | yes, macro 202207 |
yes | Pass: the full program compiles and runs, no warnings with -Wall -Wextra -Wpedantic |
| GCC 15 | per table, GCC 16 | per table, GCC 14 | Not installed; not tested |
| GCC 14.2.0 | no: header absent | yes, at -std=c++23 |
Deducing-this program passes; the full program fails on the missing header |
| GCC 13.3.0 | no: header absent | no | Both programs fail; see errors below |
| Clang 18.1.3 with libstdc++ 13 or 14 | per table, libc++ 17/18 | per table, Clang 18 | Fail: header not found |
| Clang 18.1.3 with libstdc++ 16 | per table, libc++ 17/18 | per table, Clang 18 | Fail: error inside the libstdc++ 16 headers, see below |
| Clang with libc++ 17/18 | yes, per table | yes, per table | No libc++ on this box; not tested |
| MSVC 19.39 / 19.43 | yes, per table | yes, per table | Not tested |
On GCC 14 and GCC 13, the full program fails before any of its own code is read:
fatal error: mdspan: No such file or directory
On GCC 13, the deducing-this program on its own fails with:
error: expected identifier before 'this'
That error is the compiler not knowing the explicit object parameter syntax at all. On GCC 14 and GCC 16, the same program compiles cleanly at -std=c++23 and prints n=2 const-label=n=2. At -std=c++20, GCC accepts the syntax as an extension and warns that explicit object member functions are only available with -std=c++23 or -std=gnu++23. The combined program needs -std=c++23 in any case: under the GCC 16 default dialect, which is C++20, the program fails with 'mdspan' is not a member of 'std', because the header declares the type only for C++23.
The Clang failure with the libstdc++ 16 headers is a pairing failure, not a verdict on mdspan or on Clang. Clang 18.1.3 reads the libstdc++ 16 headers and stops inside them, with an error about member access into an incomplete iterator type in stl_iterator.h, reached through iterator_concepts.h. No flag tried changed that, and the guide records it as a failure of that pairing on this box. The cppreference table cells for Clang describe libc++, which this box does not have. A reader on Clang needs libc++ 17 or later for mdspan and should verify on their own machine rather than trusting the GCC result here.
Guard the mdspan code with the feature-test macro, so a compiler with an older library fails with a readable message instead of a missing-header error:
#include <cstdio>
#if __has_include(<mdspan>)
#include <mdspan>
#endif
#include <version>
int main() {
#ifdef __cpp_lib_mdspan
std::printf("mdspan %ld\n", (long)__cpp_lib_mdspan);
#else
std::printf("no mdspan macro\n");
#endif
#ifdef __cpp_explicit_object_parameter
std::printf("explicit-object-parameter %ld\n",
(long)__cpp_explicit_object_parameter);
#else
std::printf("no explicit-object-parameter macro\n");
#endif
#ifdef __cpp_multidimensional_subscript
std::printf("multidimensional-subscript %ld\n",
(long)__cpp_multidimensional_subscript);
#else
std::printf("no multidimensional-subscript macro\n");
#endif
return 0;
}
On this box the guard prints mdspan 202207, no explicit-object-parameter macro, and multidimensional-subscript 202211 under GCC 16.2.0, and no mdspan macro with the same other two lines under GCC 14.2.0 and GCC 13.3.0. The middle line is the surprise, and it is reported as observed: GCC defines no __cpp_explicit_object_parameter macro in the versions tested, even at -std=c++23, even though deducing this compiles. Do not guard deducing-this code on that macro and expect it to detect support on GCC. Guard it by compiler version, or by compiling a probe, and keep the mdspan macro guard for the library half, where the macro is defined and reliable. The 202207L value is the one cppreference records for std::mdspan.
One build-box note, because the GCC 16 result needs explaining. The box runs Ubuntu 24.04 with GCC 13 as its system compiler and no sudo access. GCC 16.2.0 was obtained by downloading Ubuntu's own g++-16 packages for a newer Ubuntu release and extracting them into a user directory, without installing them system-wide. The compiler that ran the programs is the stock Ubuntu 16.2.0 build, and the extraction changes only where its files sit. A reader with a normal g++-16 install has the same compiler. The system assembler and linker on the box are GNU binutils 2.42, older than the 2.45 the GCC 16 packages name as a dependency; no assembler or linker failure was observed for the programs in this guide, and no claim is made beyond that. The extraction is recorded here so the result can be reproduced on a box in the same state, not as a recommended install method.
Frequently asked questions
Why not use std::span for this?
std::span is a one-dimensional view over a contiguous sequence. It has no mapping from two indices to one offset, no layout policy, and no strided access. A span over the image buffer can only be indexed linearly, so the caller does the row and channel arithmetic by hand, in every loop. mdspan moves that arithmetic into the mapping, where it is written once and checked by the observers.
Does a view own its data?
No. A view holds a data handle and a mapping, and the buffer must outlive the view. Returning a view over a local buffer from a function returns a dangling view, in the same way returning a span over a local vector does. The library in this guide keeps the buffer in the Grid object and builds views from it while the object lives.
Can deducing this be used on GCC 13?
No. GCC 13 rejects the syntax outright, with the error quoted in the compiler matrix. The floor for the explicit object parameter is GCC 14, and the feature needs -std=c++23; at -std=c++20 GCC 14 and 16 accept it only as an extension, with a warning.
Why is there no submdspan in this guide?
Because submdspan is C++26, and this guide is a C++23 guide compiled at -std=c++23. The hand-rolled sub-view in the sixth section is the C++23 form, labeled as such. When a C++26 library is available, replace it with submdspan.
What comes next in this course?
The next guide in course order is GCC 16 Defaults to C++20: A Migration Checklist for Unpinned Codebases: what breaks when the default standard becomes C++20, in what order to fix it, and how to tell a language change from a GCC-only change. It uses the GCC 16 compiler this guide installed on the build box.
Source
- cppreference, std::mdspan — the class template, the mapping and accessor policies, the observer functions, the C++26 markings on
submdspan,at, and the padded layouts, and the__cpp_lib_mdspanvalue202207L. - cppreference, std::layout_stride — the layout mapping policy with user-defined strides.
- cppreference, C++23 — the compiler-support table:
std::mdspanin GCC 16, Clang libc++ 17 (partial) and 18, MSVC 19.39; deducingthisin GCC 14, Clang 18, MSVC 19.32 (partial) and 19.43. - GCC, GCC 16 Changes —
std::mdspanunder the improved experimental support for C++23 in GCC 16. - ISciNumPy, C++23 — the MDSpan and Deducing This sections: the multidimensional subscript, the sub-view note for C++26, the four-overload collapse, and the CRTP pattern applied without a template.
Facts verified 2026-11-04. Every code listing on this page was compiled and run with GCC 16.2.0 at -std=c++23 on Ubuntu 24.04, including the programs that fail: the error messages are quoted from the actual compiler output, and the failing programs are labeled as failing. The same programs were attempted with GCC 14.2.0, GCC 13.3.0, and Clang 18.1.3, with the results in the compiler matrix. GCC 15, MSVC, and Clang with libc++ were not tested.
Keep Exploring
Keep exploring the docs, samples, and free tools to continue your evaluation.
Score SPF, DKIM, DMARC, MTA-STS, TLS-RPT, and BIMI for any domain.
Review grounded audit, compare, fix-plan, and report excerpts before you wire the API into anything.
Jump from the workflow page into the quickstart, endpoint guides, and full OpenAPI reference.