Tip of the Week #93: using absl::Span

Originally posted as TotW #93 on April 23, 2015

by Samuel Benzaquen ([email protected])

At Google we are accustomed to using absl::string_view as function parameters and return types when we want to deal with unowned strings. It can make the API more flexible and it can improve performance by avoiding unneeded conversions to std::string. (See TotW #1.)

absl::string_view has a more generic cousin called absl::Span absl::Span<const T> is to std::vector<T> what absl::string_view is to std::string. It provides a read-only interface to the elements of the vector, but it can also be constructed from non-vector types (like arrays and initializer lists) without incurring the cost of copying the elements.

The const can be dropped, so where absl::Span<const T> is a view into an array whose elements can’t be mutated, absl::Span<T> allows non-const access to the elements. Unlike spans of const, however, these require explicit construction.

Note on std::span / gsl::span

It is important to note that, while absl::Span is similar in design and purpose to the std::span proposal (and existing gsl::span reference implementation), absl::Span is not currently guaranteeing to be a drop-in replacement for any eventual standard, as the std::span proposal is still in development and undergoing changes.

Instead, absl::Span aims to have an interface as similar as possible to absl::string_view, without the string-specific functionality.

As Function Parameters

Some of the benefits of using absl::Span as a function parameter are similar to those of using absl::string_view.

The caller can pass a slice of the original vector, or pass a plain array. It is also compatible with other array-like containers, like absl::InlinedVector, absl::FixedArray, google::protobuf::RepeatedField, etc.

As with absl::string_view, it is usually better to pass absl::Span by value when used as a function parameter; this form is slightly faster than passing by const reference (on most platforms), and produces smaller code.

Example: Basic Span Usage

void TakesVector(const std::vector<int>& ints);
void TakesSpan(absl::Span<const int> ints);

void PassOnlyFirst3Elements() {
  std::vector<int> ints = MakeInts();
  // We need to create a temporary vector, and incur an allocation and a copy.
  TakesVector(std::vector<int>(ints.begin(), ints.begin() + 3));
  // No copy or allocations are made when using Span.
  TakesSpan(absl::Span<const int>(ints.data(), 3));
}

void PassALiteral() {
  // This creates a temporary std::vector<int>.
  TakesVector({1, 2, 3});
  // Span does not need a temporary allocation and copy, so it is faster.
  TakesSpan({1, 2, 3});
}
void IHaveAnArray() {
  int values[10] = ...;
  // Once more, a temporary std::vector<int> is created.
  TakesVector(std::vector<int>(std::begin(values), std::end(values)));
  // Just pass the array. Span detects the size automatically.
  // No copy was made.
  TakesSpan(values);
}

Buffer Overflow Prevention

Because absl::Span knows its own length, APIs can use it to gain safety over C-style pointer/length pairs.

Example: Safer memcpy()

// Bad code
void BadUser() {
  int src[] = {1, 2, 3};
  int dest[2];
  memcpy(dest, src, ABSL_ARRAYSIZE(src) * sizeof(int)); // oops! Dest overflowed.
}
// A simple example, but takes advantage that the sizes of the Spans are known
// and prevents the above mistake.
template <typename T>
bool SaferMemCpy(absl::Span<T> dest, absl::Span<const T> src) {
  if (src.size() > dest.size()) {
    return false;
  }
  memcpy(dest.data(), src.data(), src.size());
  return true;
}

void GoodUser() {
  int src[] = {1, 2, 3}, dest[2];
  // No overflow!
  SaferMemCpy(absl::MakeSpan(dest), absl::Span<const int>(src));
}

Const Correctness for Vector of Pointers

A big problem with passing around std::vector<T*> is that you can’t make the pointees const without changing the type of the container.

Any function taking a const std::vector<T*>& will not be able to modify the vector, but it can modify the T values. This also applies to accessors that return a const std::vector<T*>&. You can’t prevent the caller from modifying the T values.

Common “solutions” include copying or casting the vector into the right type. These solutions are slow (for the copy) or undefined behavior (for the cast) and should be avoided. Instead, use absl::Span.

Example: Function Parameter

Consider these Frob variants:

void FrobFastWeak(const std::vector<Foo*>& v);
void FrobSlowStrong(const std::vector<const Foo*>& v);
void FrobFastStrong(absl::Span<const Foo* const> v);

Starting with a const std::vector<Foo*>& v that needs frobbing, you have two imperfect options and one good one.

// fast and easy to type but not const-safe
FrobFastWeak(v);
// slow and noisy, but safe.
FrobSlowStrong(std::vector<const Foo*>(v.begin(), v.end()));
// fast, safe, and clear!
FrobFastStrong(v);

Example: Accessor

// Bad code
class DontDoThis {
 public:
  // Don’t modify my Foos, pretty please.
  const std::vector<Foo*>& shallow_foos() const { return foos_; }

 private:
  std::vector<Foo*> foos_;
};

void Caller(const DontDoThis& my_class) {
  // Modifies a foo even though my_class is a reference-to-const
  my_class->foos()[0]->SomeNonConstOp();
}
// Good code
class DoThisInstead {
 public:
  absl::Span<const Foo* const> foos() const { return foos_; }

 private:
  std::vector<Foo*> foos_;
};

void Caller(const DoThisInstead& my_class) {
  // This one doesn't compile.
  // my_class.foos()[0]->SomeNonConstOp();
}

Conclusion

When used appropriately, absl::Span can provide decoupling, const correctness and a performance benefit.

It is important to note that absl::Span behaves much like absl::string_view by being a reference to some externally owned data. All the same warnings apply. In particular, an absl::Span must not outlive the data it refers to.


Subscribe to the Abseil Blog