Guidelines Support Library Review: span<T>

The Guidelines Support Library is a Microsoft implementation of some of the types and functions described in the C++ Core Guidelines maintained by the Standard C++ Foundation. Among the types provided by the GSL is span<T> formerly known as array_view<T>. This article is an introduction to this type.

span<T> is a non-owning range of contiguous memory recommended to be used instead of pointers (and size counter) or standard containers (such as std::vector or std::array).

Suppose you want to create a function that displays the content of a container. Such a function could look like this:

template <typename T>
void display(std::vector<T> const & data)
{
   for (auto const e : data) std::cout << e << ' ';
   std::cout << std::endl;
}

std::vector<int> v{ 1, 2, 3, 4, 5 };
display(v);

This will work with vectors, but not with arrays or lists. So then you need overloads in order to support other containers.

template <class Iterator>
void display(Iterator const b, Iterator const e)
{
   for (Iterator i = b; i != e; ++i) std::cout << *i << ' ';
   std::cout << std::endl;
}

template <typename T>
void display(std::vector<T> const & data)
{
   display(std::begin(data), std::end(data));
}

template <typename T, size_t S>
void display(std::array<T, S> const & data)
{
   display(std::begin(data), std::end(data));
}

std::vector<int> v{ 1, 2, 3, 4, 5 };
display(v);

std::array<int, 5> a{1, 2, 3, 4, 5};
display(a);

But what if you now what to display the content of an int[] or an int*?

The span<T> type is intended as a uniform interface over arrays, pointers and standard containers that can be used as a replacement of these types. It does not store a copy of the original data, only a pointer to data and counters.

template <typename T>
void display(gsl::span<T> const & data)
{
   for(auto const e : data) std::cout << e << ' ';
   std::cout << std::endl;
}

std::vector<int> v{ 1, 2, 3, 4, 5 };
gsl::span<int> s1{ v };
display(s1);

std::array<int, 5> a{1, 2, 3, 4, 5};
gsl::span<int> s2{ a };
display(s2);

int arr[] = { 1, 2, 3, 4, 5 };
gsl::span<int> s3{ arr };
display(s3);

The following helper functions are used in the samples below:

template <class Iterator>
void show(Iterator b, Iterator e)
{
   for (auto i = b; i != e; ++i)
   {
      std::cout << *i << ' ';
   }
   std::cout << std::endl;
}

template <class T>
void show(gsl::span<T> const & s)
{
   show(std::begin(s), std::end(s));
}

template <class T, size_t R>
void show(gsl::span<T, R> const & s)
{
   show(std::begin(s), std::end(s));
}

template <class T, size_t R, size_t C>
void show(gsl::span<T, R, C> const & s)
{
   show(std::begin(s), std::end(s));
}

template <class T, size_t R, size_t C>
void show(gsl::span<T, gsl::dynamic_range, R, C> const & s)
{
   show(std::begin(s), std::end(s));
}

Creating a span

A span can be created in many ways, including:

  • from a single value (variable, not a literal)
    int n = 1;
    gsl::span<int>    s1 = n; // s1 = {1}
    gsl::span<int, 1> s2 = n; // se = {1}
  • from a pointer and number of elements
    std::vector<int> v = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
    gsl::span<int>    s1(v.data(), 4); // s1 = {1, 2, 3, 4}
    gsl::span<int, 4> s2(v.data(), 4); // s2 = {1, 2, 3, 4}
  • from a begin and end pointer
    std::vector<int> v = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
    gsl::span<int>    s1(&v[0], &v[4]); // s1 = {1, 2, 3, 4}
    gsl::span<int, 4> s2(&v[0], &v[4]); // s2 = {1, 2, 3, 4}
  • from a C-like array
    int arr[] = { 1, 2, 3, 4, 5 };
    gsl::span<int>    s1{ arr }; // s1 = {1, 2, 3, 4, 5}
    gsl::span<int, 5> s2{ arr }; // s2 = {1, 2, 3, 4, 5}
    
    int arr2[2][3] = {1, 2, 3, 4, 5, 6};
    gsl::span<int>       s3{ arr2 }; // s3 = {1, 2, 3, 4, 5, 6}
    gsl::span<int, 6>    s4{ arr2 }; // s3 = {1, 2, 3, 4, 5, 6}
    gsl::span<int, 2, 3> s5{ arr2 }; // s3 = {1, 2, 3, 4, 5, 6}
  • from a dynamic array
    int (*arr)[3][3] = new int[2][3][3];
    for (int i = 0; i < 2; ++i)
    {
       for (int j = 0; j < 3; j++)
       {
          for (int k = 0; k < 3; ++k)
          {
             arr[i][j][k] = k + j * 3 + i * 9;
          }
       }
    }
    
    gsl::span<int, gsl::dynamic_range, 3, 4> s1{ arr, 2 };
    show(s1);  // prints 0 1 2 3 4 .. 17
    
    delete[] arr;
  • from a standard container with contiguous memory layout such as array, vector or string
    std::array<int, 5> a = { 1, 2, 3, 4, 5 };
    gsl::span<int>    s1{ a }; // s1 = {1, 2, 3, 4, 5, 6}
    gsl::span<int, 5> s2{ a }; // s2 = {1, 2, 3, 4, 5, 6}
    
    std::vector<int> v = { 1, 2, 3, 4, 5 };
    gsl::span<int>    s3{ v }; // s3 = {1, 2, 3, 4, 5, 6}
    gsl::span<int, 5> s4{ v }; // s4 = {1, 2, 3, 4, 5, 6}
    
    std::string text = "sample";
    gsl::span<const char> s5{ text }; // s5 = {'s', 'a', 'm', 'p', 'l', 'e'}
  • using the gsl::as_span() function:
    std::vector<int> v = { 1, 2, 3, 4, 5 };
    auto sv = gsl::as_span(v);  // sv = {1, 2, 3, 4, 5}
    
    std::array<int, 5> a = { 1, 2, 3, 4, 5 };
    auto sa = gsl::as_span(a);  // sa = {1, 2, 3, 4, 5}

Notice that it is not possible to create a span from an initializer_list because an initializer list is a temporary object and a span is a non-owning container, it does not make a copy of the data, and therefore it can end up containing dangling references to temporary data. For a detailed discussion on the topic see this issue.

Size of a span

A span can have zero, one or more dimensions, and each dimension can have a different size (number of elements). The number of dimensions is called rank and the number of elements in a dimension is called extent. You can retrieve the rank and extent using the functions with the same name.

gsl::span<int> s0{ nullptr }; // rank = 1, extent[0] = 0
gsl::span<int> s1;            // rank = 1, extent[0] = 0 

int arr[] = { 1, 2, 3, 4, 5 };
gsl::span<int> s2{ arr };     // rank = 1, extent[0] = 5
gsl::span<int, 5> s3{ arr };  // rank = 1, extent[0] = 5

std::cout << "rank=" << s2.rank() << std::endl;           // rank=1
std::cout << "extent(0)=" << s2.extent(0) << std::endl;   // extent[0]=5
std::cout << "extent<0>=" << s2.extent<0>() << std::endl; // extent[0]=5

gsl::span<int, 2, 3> s4{ arr, 6 }; // rank = 2, extent[0] = 2, extent[1] = 3

std::cout << "rank=" << s4.rank() << std::endl;           // rank=2
std::cout << "extent(0)=" << s4.extent(0) << std::endl;   // extent[0]=2
std::cout << "extent(1)=" << s4.extent(1) << std::endl;   // extent[1]=3

Subspans

It is possible to create subspans from a span. There are several functions that do that:

  • first(): returns the sub-span with the first N elements from the original span
  • last(): returns the sub-span with the last N elements from the original span
  • subspan(): returns the sub-span within the specified range (first and last positions) of the original span.
int arr[] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
gsl::span<int> s{ arr };

auto fs1 = s.first(0);        // fs1 = {}
auto fs2 = s.first(5);        // fs2 = {1, 2, 3, 4, 5}
auto fs3 = s.first<3>();      // fs3 = {1, 2, 3}

auto ls1 = s.last(0);         // ls1 = {}
auto ls2 = s.last(5);         // ls2 = {6, 7, 8, 9, 10} 
auto ls3 = s.last<3>();       // ls3 = {8, 9, 10}

auto ns1 = s.subspan(0, 0);   // ns1 = {}
auto ns2 = s.subspan(0, 5);   // ns2 = {1, 2, 3, 4, 5}
auto ns3 = s.subspan(5, 0);   // ns3 = {}
auto ns4 = s.subspan<0, 0>(); // ns4 = {}
auto ns5 = s.subspan<0, 5>(); // ns5 = {1, 2, 3, 4, 5}
auto ns6 = s.subspan<5, 0>(); // ns6 = {}

Comparisons

You can use the comparison operators (==, !=, <, <=, >, >=) with two spans. Equality is checked with std::equal (two ranges are equal if every element in the first range is equal to the element corresponding to the same position in the second range) and less/greater is checked with std::lexicographical_compare() (one range is less than another if the first mismatch element in the first range is less than the element on the same position in the second range).

int arr1[2][3] = { 1, 2, 3, 4, 5, 6 };
gsl::span<int, 6> s1{ arr1 };

int arr2[] = { 1, 2, 3, 4, 5, 6 };
gsl::span<int> s2{ arr2 };

int arr3[] = { 1, 2, 3 };
gsl::span<int> s3{ arr3 };

int arr4[] = { 3, 2, 1 };
gsl::span<int> s4{ arr4 };

std::cout << (s1 == s2) << std::endl; // prints 1
std::cout << (s2 == s3) << std::endl; // prints 0
std::cout << (s2 > s3)  << std::endl; // prints 1
std::cout << (s4 != s3) << std::endl; // prints 1
std::cout << (s4 > s3)  << std::endl; // prints 1

Element access

It is possible to access the content of a span either with iterators or indexes.

std::vector<int> v = { 1, 2, 3, 4, 5, 6 };
gsl::span<int> s{ v };

// prints 1 2 3 4 5 6
for (auto const & e : s) std::cout << e << std::endl;
// prints 6 5 4 3 2 1
for (auto it = s.rbegin(); it != s.rend(); ++it) std::cout << *it << std::endl;
// prints 1 2 3 4 5 6
for (auto it = std::begin(s); it != std::end(s); ++it) std::cout << *it << std::endl;

When it comes to index access you can either index like a regular array (s[0], s[1][2], etc.) or using a special type called index.

std::vector<int> v = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12};
gsl::span<int> s1{ v };
gsl::span<int, 3, 4> s2{ v };

// prints 1 2 3 ... 12
for (int i = 0; i < s1.extent(0); ++i) 
   std::cout << s1[i] << ' ';
std::cout << std::endl;

// prints 
// 1 2 3 4
// 5 6 7 8
// 9 10 11 12
for (int i = 0; i < s2.extent(0); ++i)
{
   for (int j = 0; j < s2.extent(1); ++j)
      std::cout << s2[i][j] << ' ';
   std::cout << std::endl;
}

// uni-dimensional index
gsl::index<1> i1 = { 0 };
std::cout << s1[i1] << std::endl;  // prints 1

// bi-dimensional index
gsl::index<2> i2 = { 1, 1 };
std::cout << s2[i2] << std::endl;  // prints 6

// bi-dimensional index computed from another index
gsl::index<2> i3 = i2 * 2; 
std::cout << s2[i3] << std::endl;  // prints 11

6 thoughts on “Guidelines Support Library Review: span<T>”

  1. “span is a non-owning range of contiguous memory recommended to be used instead ”

    why “contiguous”? The only thing above which suggests the need for this requirement is that the span-view be accessible via an index. But there are lots of application which don’t call for this. So this “requirement” should be lowered to the status of “option”

    In my view – creating a new concept “span” isn’t really of much value as compared to the more traditionally used “[const] view” which has the benefit of being more intuitive. I don’t think this idea is really adding anything of value.

    Reply
  2. Thanks for the nice overview! I have one question:
    What is the reason of using const gsl::span& as a parameter? It’s a shallow type and has similar semantic as initializer_list, so I would rather prefer passing it as a value.

    Reply
  3. >> But what if you now what to display the content of an int[] or an int*?

    Actually, `template void display(Iterator const b, Iterator const e)` works very well with raw pointers/arrays. To be honest, the overloads for vector and array are misleading and don’t really fit in the STL iterator/algorithm concept.

    Instead, the reasoning behind gsl::span is that it should represent a non-owning contiguous container, as opposed to std::vector – another step in getting rid of raw pointers and making the code cleaner/clearer, I guess.

    Reply

Leave a Comment