std::span in C++20: More Details
A std::span
represents an object that refers to a contiguous sequence of objects. Today, I want to write about its not-so-obvious features.
A std::span
, sometimes also called a view, is never an owner. This contiguous memory can be a plain array, a pointer with a size, a std::array
, a std::vector
, or a std::string
. A typical implementation consists of a pointer to its first element and a size. The main reason for having a std::span<T>
is that a plain array will decay to a pointer if passed to a function; therefore, the size is lost. This decay is a typical reason for errors in C/C++.
Automatically deduces the size of a contiguous sequence of objects
In contrast, std::span<T>
automatically deduces the size of contiguous sequences of objects.
// printSpan.cpp #include <iostream> #include <vector> #include <array> #include <span> void printMe(std::span<int> container) { std::cout << "container.size(): " << container.size() << '\n'; // (4) for(auto e : container) std::cout << e << ' '; std::cout << "\n\n"; } int main() { std::cout << std::endl; int arr[]{1, 2, 3, 4}; // (1) printMe(arr); std::vector vec{1, 2, 3, 4, 5}; // (2) printMe(vec); std::array arr2{1, 2, 3, 4, 5, 6}; // (3) printMe(arr2); }
The C-array (1), std::vector
(2), and the std::array
(3) have int
‘s. Consequently, std::span
also holds int
’s. There is something more interesting in this simple example. For each container, std::span
can deduce its size (4).
This was a short reminder about std::span
. For the full story, read my previous post “std::span in C++20: Bounds-Safe Views of Sequences of Objects“.
A std::span
can have a static extent or a dynamic extent.
Static versus Dynamic Extent
By default, std::span
has a dynamic extent:
template <typename T, std::size_t Extent = std::dynamic_extent> class span;
When a std::span
has a static extent, its size is known at compile time and part of the type: std::span
. Consequently, its implementation needs only a pointer to the first element of the contiguous sequence of objects.
Implementing a std::span
with a dynamic extent consists of a pointer to the first element and the size of the contiguous sequence of objects. The size is not part of the std::span
type.
The next example emphasizes the differences between the two kinds of spans.
Modernes C++ Mentoring
Do you want to stay informed: Subscribe.
// staticDynamicExtentSpan.cpp #include <iostream> #include <span> #include <vector> void printMe(std::span<int> container) { // (3) std::cout << "container.size(): " << container.size() << '\n'; for (auto e : container) std::cout << e << ' '; std::cout << "\n\n"; } int main() { std::cout << '\n'; std::vector myVec1{1, 2, 3, 4, 5}; std::vector myVec2{6, 7, 8, 9}; std::span<int> dynamicSpan(myVec1); // (1) std::span<int, 4> staticSpan(myVec2); // (2) printMe(dynamicSpan); printMe(staticSpan); // staticSpan = dynamicSpan; ERROR // (4) dynamicSpan = staticSpan; // (5) printMe(staticSpan); // (6) std::cout << '\n'; }
dynamicSpan
(line 1) has a dynamic extent, while staticSpan
(line 2) has a static extent. Both std::span
s return their size in the printMe
function (line 3). A std::span
with static extent can be assigned to a std::span
with dynamic extent, but not vice versa. Line 4 would cause an error, but lines 5, and 6 are valid.
There is one particular use case of std::span
. A std::span
can be a constant range of modifiable elements.
A Constant Range of Modifiable Elements
For simplicity, I name a std::vector
and a std::span range
. A std::vector
models a modifiable range of modifiable elements: std::vector
. When you declare this std::vector
as const
, it models a constant range of constant objects: const std::vector
. You cannot model a constant range of modifiable elements. This is where std::span
comes into play. A std::span
models a constant range of modifiable objects: std::span
. The following image emphasizes the variations of (constant/modifiable) ranges and (constant/modifiable) elements.
// constRangeModifiableElements.cpp #include <iostream> #include <span> #include <vector> void printMe(std::span<int> container) { std::cout << "container.size(): " << container.size() << '\n'; for (auto e : container) std::cout << e << ' '; std::cout << "\n\n"; } int main() { std::cout << '\n'; std::vector<int> origVec{1, 2, 2, 4, 5}; // Modifiable range of modifiable elements std::vector<int> dynamVec = origVec; // (1) dynamVec[2] = 3; dynamVec.push_back(6); printMe(dynamVec); // Constant range of constant elements const std::vector<int> constVec = origVec; // (2) // constVec[2] = 3; ERROR // constVec.push_back(6); ERROR std::span<const int> constSpan(origVec); // (3) // constSpan[2] = 3; ERROR // Constant range of modifiable elements std::span<int> dynamSpan{origVec}; // (4) dynamSpan[2] = 3; printMe(dynamSpan); std::cout << '\n'; }
The vector dynamVec
(line 1) is a modifiable range of modifiable elements. This observation does not hold for the vector constVec
(line 2). Neither can constVec
change its elements nor its size. constSpan
(line 3) behaves accordingly. dynamSpan
(line 4) models the unique use case of a constant range of modifiable elements.
Finally, I want to mention two dangers you should know when using std::span
.
Dangers of std::span
The typical issues of std::span
are twofold. First, a std::span
should not act on a temporary and second, the size of the underlying contiguous range of a std::span
should not be modified.
A std::span
on a Temporary
A std::span
is never an owner. Therefore, a std::span
does not extend the lifetime of its underlying data. Consequently, a std::span should only operate on an lvalue. Using std::span
on a temporary range is undefined behavior.
// temporarySpan.cpp #include <iostream> #include <span> #include <vector> std::vector<int> getVector() { // (2) return {1, 2, 3, 4, 5}; } int main() { std::cout << '\n'; std::vector<int> myVec{1, 2, 3, 4, 5}; // (1) std::span<int, 5> mySpan1{myVec}; std::span<int, 5> mySpan2{getVector().begin(), 5}; // (3) for (auto v: std::span{myVec}) std::cout << v << " "; std::cout << '\n'; for (auto v: std::span{getVector().begin(), 5}) std::cout << v << " "; // (4) std::cout << "\n\n"; }
Using a std::span
with a static extent or a std::span
with a dynamic extent on the lvalue is fine. When I switch from the lvalue std::vector
in line 1 to a temporary std::vector
, given by the function getVector
(lines 2), the program has undefined behavior. Both lines 3 and 4 are not valid. Consequently, executing the program exposes the undefined behavior. The output of line 4 does not match with the std::vector
, generated by the function getVector().
Changing the Size of the Underlying Contiguous Range
When you change the size of the underlying contiguous range, the contiguous range may be reallocated, and the std::span
refers to stale data.
std::vector<int> myVec{1, 2, 3, 4, 5}; std::span<int> sp1{myVec}; myVec.push_back(6); // undefined behavior
What’s Next?
In my next post, I dive once more in the formatting library of C++20.
Thanks a lot to my Patreon Supporters: Matt Braun, Roman Postanciuc, Tobias Zindl, G Prvulovic, Reinhold Dröge, Abernitzke, Frank Grimm, Sakib, Broeserl, António Pina, Sergey Agafyin, Андрей Бурмистров, Jake, GS, Lawton Shoemake, Jozo Leko, John Breland, Venkat Nandam, Jose Francisco, Douglas Tinkham, Kuchlong Kuchlong, Robert Blanch, Truels Wissneth, Mario Luoni, Friedrich Huber, lennonli, Pramod Tikare Muralidhara, Peter Ware, Daniel Hufschläger, Alessandro Pezzato, Bob Perry, Satish Vangipuram, Andi Ireland, Richard Ohnemus, Michael Dunsky, Leo Goodstadt, John Wiederhirn, Yacob Cohen-Arazi, Florian Tischler, Robin Furness, Michael Young, Holger Detering, Bernd Mühlhaus, Stephen Kelley, Kyle Dean, Tusar Palauri, Juan Dent, George Liao, Daniel Ceperley, Jon T Hess, Stephen Totten, Wolfgang Fütterer, Matthias Grün, Phillip Diekmann, Ben Atakora, Ann Shatoff, Rob North, Bhavith C Achar, Marco Parri Empoli, Philipp Lenk, Charles-Jianye Chen, Keith Jeffery, Matt Godbolt, and Honey Sukesan.
Thanks, in particular, to Jon Hess, Lakshman, Christian Wittenhorst, Sherhy Pyton, Dendi Suhubdy, Sudhakar Belagurusamy, Richard Sargeant, Rusty Fleming, John Nebel, Mipko, Alicja Kaminska, Slavko Radman, and David Poole.
My special thanks to Embarcadero | |
My special thanks to PVS-Studio | |
My special thanks to Tipi.build | |
My special thanks to Take Up Code | |
My special thanks to SHAVEDYAKS |
Modernes C++ GmbH
Modernes C++ Mentoring (English)
Rainer Grimm
Yalovastraße 20
72108 Rottenburg
Mail: schulung@ModernesCpp.de
Mentoring: www.ModernesCpp.org
Modernes C++ Mentoring,