Span Vs Vector

I occasionally tune into "C++ Weekly" on Youtube, and this is today's video: https://www.youtube.com/watch?v=zCzD9uSDI8c "C++ Weekly - Ep 361 - Is A Better `main` Possible?"

1) I mostly code in C++11 or earlier, so can someone who prefers span over vector please explain why it is your preferred container and/or when is it the better option?

2) I don't generally use smart pointers, but from my reading I got that a span will not keep a smart pointer alive, so how do you deal with that? Or is that the behavior that you are looking for?
Last edited on
1) A span isn't a replacement for a vector in general. A vector stores the data it contains. A span does NOT, it is just a pointer surrogate or a reference to data that you already stored elsewhere. So if the choice is making an extra copy of data into a new vector or using a span, you use the span, to avoid the copy. If the choice is what container to use, span is NOT a container...

2) situational. I am struggling to find a reason to use a span on a pointer in the first place, but if you did, which behavior you want is part of your design. You can always stuff the span into your own class and add a smart pointer to the class and hold onto it there as a locked reference, for a very simple solution. I know thou shalt not and all that, but you could even inherit the span into your class for something so simple, and its extra member is just silent and hidden, and nothing much to be done beyond the wrapper. Or maybe nothing needs to be done, if the pointer is alive, and you span it and iterate it, the span ends, pointer still alive... nothing to worry about.
Last edited on
I haven't really started using C++20 yet so I don't know exactly when I will use std::span.

I think it's quite similar to std::string_view except that it allows you to modify the underlying data unless you explicitly mark the element type as const (e.g. std::span<const T>).

One use case seems to be as a function parameter. By using std::span I can pass std::vectors, std::arrays and even raw arrays.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include <iostream>
#include <vector>
#include <array>
#include <span>

void print(std::span<const int> numbers)
{
    for (int value : numbers)
    {
        std::cout << value << "\n";
    }
}

int main()
{
	std::vector<int> vec = {1, 2, 3};
	print(vec);
	
	std::array<int, 3> arr = {4, 5, 6};
	print(arr);
	
	int arr2[] = {7, 8, 9};
	print(arr2);
}


You could of course do it the C way and just take a pointer and a size as argument.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include <iostream>
#include <vector>
#include <array>
#include <span>

void print(const int* numbers, std::size_t size)
{
    for (std::size_t i = 0; i < size; ++i)
    {
        std::cout << numbers[i] << "\n";
    }
}

int main()
{
	std::vector<int> vec = {1, 2, 3};
	print(vec.data(), vec.size());
	
	std::array<int, 3> arr = {4, 5, 6};
	print(arr.data(), vec.size());
	
	int arr2[] = {7, 8, 9};
	print(arr2, std::size(arr2));
}


To make it more convenient to pass std::vectors and std::arrays you could create some helper functions.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
#include <iostream>
#include <vector>
#include <array>
#include <span>

void print(const int* numbers, std::size_t size)
{
    for (std::size_t i = 0; i < size; ++i)
    {
        std::cout << numbers[i] << "\n";
    }
}

void print(const std::vector<int>& numbers)
{
    print(numbers.data(), numbers.size());
}

template <std::size_t N>
void print(const std::array<int, N>& numbers)
{
    print(numbers.data(), numbers.size());
}

int main()
{
	std::vector<int> vec = {1, 2, 3};
	print(vec);
	
	std::array<int, 3> arr = {4, 5, 6};
	print(arr);
	
	int arr2[] = {7, 8, 9};
	print(arr2, std::size(arr2));
}

If you want to pass a subrange of a container you would still have to use the pointer-and-size version.

But this is not as general as using a function template with iterators.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include <iostream>
#include <vector>
#include <array>

template <typename Iter>
void print(Iter begin, Iter end)
{
	for (Iter it = begin; it != end; ++it)
	{
		std::cout << *it << "\n";
	}
}

int main()
{
	std::vector<int> vec = {1, 2, 3};
	print(vec.begin(), vec.end());
	
	std::array<int, 3> arr = {4, 5, 6};
	print(arr.begin(), arr.end());
	
	int arr2[] = {7, 8, 9};
	print(std::begin(arr2), std::end(arr2));
}

This allows you to use any container, not just random-access containers (It also allow you to use other element types than int).

To make it possible to pass containers directly without having to use begin()/end() you could create a "helper function".

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
#include <iostream>
#include <vector>
#include <array>

template <typename Iter>
void print(Iter begin, Iter end)
{
	for (Iter it = begin; it != end; ++it)
	{
		std::cout << *it << "\n";
	}
}

template <typename Container>
void print(Container& c)
{
	print(std::begin(c), std::end(c));
}

int main()
{
	std::vector<int> vec = {1, 2, 3};
	print(vec);
	
	std::array<int, 3> arr = {4, 5, 6};
	print(arr);
	
	int arr2[] = {7, 8, 9};
	print(arr2);
}


This makes me a bit unsure in what situation I would really want to use std::span as a function parameter.
Last edited on
newbieg wrote:
I don't generally use smart pointers, but from my reading I got that a span will not keep a smart pointer alive, so how do you deal with that? Or is that the behavior that you are looking for?

std::span is more like a "raw" pointer (not a "smart" pointer). You use it to refer to something else that you "own" or that you know will stay alive for as long as you use it.

This means it's generally safe to use std::span as function parameter, the same way that it's generally safe to use references (and pointers) as function parameters.

If you create a vector (or array) as a local variable inside a function (or as a member of a class) then you could use std::span to refer to portions of that vector. As long as the std::spans don't outlive the function (or class object), and you make sure the vector is not reallocated while using the std::spans, then you should be fine.
Last edited on
There are interesting things going on there, especially that it can make semi-generic functions. I good amount of code that I can use span in, and it's certainly worth keeping an eye open for chances to use it.
Thank you for the very in-depth answers.
Last edited on
yes, I have often used the pointer & size approach, span makes that cleaner.
std::span makes passing containers, including regular arrays, less of a pain.

https://www.modernescpp.com/index.php/c-20-std-span
std::span is a viable option only for containers which store the elements in contiguous memory; a pair of iterators or a range has no such restriction and is generally more flexible.

std::span is essentially a vocabulary type. From the proposal:
The span class is expected to become a frequently used vocabulary type in function interfaces (as a safer replacement of “(pointer, length)” idioms), ...
https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2018/p0122r7.pdf

Thank you JLBorges, I checked out the pdf you linked, and it really had a very good explanation of the whole intent for the span type. It backs up what everyone has said, but it also helped to line things out.

While I was on that site I also grabbed a copy of the C++23 "Working Draft", I wasn't aware of how to get it before. (Even the draft doesn't give an intent, something that says "This is why we need this", so that PDF was excellent.)
I'm certainly bookmarking the site.
Last edited on
A generated html version of the current working draft: http://eel.is/c++draft/

DIS of earlier versions (C++ 11, 14, 17, 20): https://github.com/timsong-cpp/cppwp
Topic archived. No new replies allowed.