C++ Interview: How Does std::mdspan Handle Layout, Lifetime, and Performance?
Question and scenario
Design a two-dimensional matrix interface that avoids copying, supports row-major and column-major data, and explains std::mdspan layout, lifetime, and performance risks.
This fits C++23, numerical computing, image processing, and high-performance service interviews. The interviewer wants you to connect a two-dimensional index to linear storage, ownership, and access patterns instead of naming a container from memory.
What the interviewer is testing
- Whether you know that
std::mdspanis a non-owning multidimensional view and does not allocate or release elements. - Whether you can explain the mapping differences among
layoutright,layoutleft, and custom strides. - Whether you can identify dangling-view risks from the lifetime, growth, or relocation of the backing buffer.
- Whether you can connect layout choice, traversal order, and cache locality.
- Whether you can express an interface contract with static extents, dynamic extents, and runtime checks.
Clarifying questions before answering
- Who owns the backing data, and may the caller grow or move the container while the view exists?
- Are the row and column extents known at compile time, and must one function accept many shapes?
- Is the input row-major, column-major, or padded and tiled?
- Is the performance target sequential scanning, random access, or portability across execution devices?
30-second answer framework
mdspan is a C++23 non-owning multidimensional array reference. It stores a data handle, extents, and a mapping from indices to offsets. First keep the owner alive and address-stable. Then choose layoutright (unit stride in the rightmost dimension, common C-style row-major behavior), layoutleft (unit stride in the leftmost dimension, common column-major behavior), or layout_stride. Static extents can be encoded in the type; dynamic extents use dextents. The caller still owns the buffer-size and lifetime contract. Traversal should follow the contiguous dimension, because a mismatched walk can be correct but cache-unfriendly.
Step-by-step deep answer
1. Separate the view from the container
mdspan is a multidimensional counterpart to one-dimensional span: it provides indexing, extents, and mapping, but it does not own elements. The owner is storage here:
#include <mdspan>
#include <vector>
std::size_t rows = 3;
std::size_t cols = 4;
std::vector<float> storage(rows * cols);
using matrix_view = std::mdspan<float, std::dextents<std::size_t, 2>>;
matrix_view matrix(storage.data(), rows, cols);
matrix(1, 2) = 7.0f;After storage is destroyed, moved, or reallocated, matrix cannot be used. The interface must forbid those operations or accept only a pointer and size whose storage remains stable for the call.
2. Explain the layout mapping
The layout policy maps logical coordinates to a linear offset. layoutright makes the rightmost extent contiguous, so a two-dimensional walk commonly scans row then column. layoutleft makes the leftmost extent contiguous. layout_stride can represent padding, transposed views, slices, or tiled storage.
The layout does not copy data or convert row-major data into column-major data. If the mapping disagrees with the real buffer, the view reads the wrong elements. If the mapping is correct but traversal fights the stride, the main cost is poor cache locality.
3. Choose static or dynamic extents
An invariant such as a four-column matrix can be encoded in the type:
using four_column_view = std::mdspan<
float,
std::extents<std::size_t, std::dynamic_extent, 4>>;The row count is runtime data while the column count is a type-level contract. For a fully dynamic shape, use the two-dimensional dextents alias. In either case, check before construction that rows * cols fits the actual buffer and guard against multiplication overflow.
4. Turn performance claims into stride measurements
Show two loops: scan a layoutright view by rows so the inner column index is contiguous; for layoutleft, reverse the emphasis. Inspect matrix.mapping().stride(i) or the mapping's required span size so that “cache friendly” becomes a measurable hypothesis. Benchmark sequential, reverse, and strided walks with fixed data sizes and compiler options.
5. State the bounds and accessor boundary
Default element access is not an automatic bounds-checking guarantee. If the product contract needs checks, validate dimensions at the boundary or provide an accessor policy with checking semantics; do not treat a debug assertion as production safety. A custom accessor can model device pointers or proxy references, but its access and lifetime contract should be stated before implementation details.
High-quality sample answer
I would leave matrix ownership with a vector, array, or caller-managed buffer and pass mdspan as a lightweight view to algorithms. At construction, record both extents and validate their product. The view must not outlive the storage or cross an operation that reallocates a vector. The layout policy must match the real stride: layoutright fits rightmost-dimension-contiguous C-style data, layoutleft fits column-contiguous data, and layout_stride handles padding. The loop order follows the contiguous dimension, so a correct mapping does not become a slow strided walk. I would use AddressSanitizer to catch dangling views, dimension and stride assertions to verify the mapping, and a fixed benchmark to compare layouts and loop orders.
Common errors
- Treating
mdspanas an owning matrix container and forgetting the owner. - Applying the default layout to external data without checking its actual stride.
- Saying “row-major is faster” without tying speed to traversal order and the contiguous dimension.
- Ignoring vector growth, relocation, or the end of a temporary array.
- Checking only two extents while ignoring buffer capacity and multiplication overflow.
- Claiming that
mdspanautomatically checks bounds or transposes data.
Follow-up questions and responses
How is mdspan different from span?
span describes one contiguous one-dimensional range. mdspan additionally carries multidimensional extents, a layout mapping, and an accessor, so it can map multidimensional coordinates to a linear offset. Neither owns the underlying elements.
When would you choose layout_stride?
Use it for padding, transposed views, slices, or non-contiguous dimensions. First establish each real stride, then verify that the largest mapped offset stays within the buffer.
Can a function return a view?
Yes, if the owner remains alive and address-stable for the entire use of the returned view. Never return a view of a local vector, and do not permit reallocation while the view is used.
How do you prove a layout choice helps?
Record strides and access order, then measure throughput, cache counters, and scaling for contiguous and strided walks. Keep the compiler, optimization level, and input distribution fixed; the type name alone is not evidence.
Sources: cppreference std::mdspan and header entries, WG21 P0009R6 “mdspan: A Non-Owning Multidimensional Array Reference,” and Verve AI’s multidimensional-array interview discussion (full URLs are recorded in meta.json).