c++vectorc++20std-span

Why in particular should I rather pass a std::span than a std::vector& to a function?


I know this might overlap with the question What is a “span” and when should I use one?, but I think the answer to this specific part of the question is pretty confusing. On one hand, there are quotes like this:

Don't use it if you have a standard library container (or a Boost container etc.) which you know is the right fit for your code. It's not intended to supplant any of them.

But in the same answer, this statement occurs:

is the reasonable alternative to passing const vector& to functions when you expect your data to be contiguous in memory. No more getting scolded by high-and-mighty C++ gurus!

So what part am I not getting here? When would I do this:

void foo(const std::vector<int>& vec) {}

And when this?

void foo(std::span<int> sp) {}

Also, would this

void foo(const std::span<int> sp) {}

make any sense? I figured that it shouldn't, because a std::span is just a struct, containing a pointer and the length. But if it doesn't prevent you from changing the values of the std::vector you passed as an argument, how can it replace a const std::vector<T>&?


Solution

  • The equivalent of passing a std::vector<int> const& is not std::span<int> const, but rather std::span<int const>. The span itself being const or not won't really change anything, but more const is certainly good practice.

    So when should you use it?

    I would say that it entirely depends on the body of the function, which you omitted from your examples.

    For example, I would still pass a vector around for this kind of functions:

    std::vector<int> stored_vec;
    
    void store(std::vector<int> vec) {
        stored_vec = std::move(vec);
    }
    

    This function does store the vector, so it needs a vector. Here's another example:

    void needs_vector(std::vector<int> const&);
    
    void foo(std::vector<int> const& vec) {
        needs_vector(vec);
    }
    

    As you can see, we need a vector. With a span you would have to create a new vector and therefore allocate.


    For this kind of functions, I would pass a span:

    auto array_sum(std::span<int const> const values) -> int {
        auto total = int{0};
    
        for (auto const v : values) {
            total += v;
        }
    
        return total;
    }
    

    As you can see, this function don't need a vector.

    Even if you need to mutate the values in the range, you can still use span:

    void increment(std::span<int> const values) {
        for (auto& v : values) {
            ++v;
        }
    }
    

    For things like getter, I will tend to use a span too, in order to not expose direct references to members from the class:

    struct Bar {
        auto get_vec() const -> std::span<int const> {
            return vec;
        }
    
    private:
        std::vector<int> vec;
    };