Sökresultat för

Så här använder du span-view i C++

16 minuter i lästid
Jens Riboe
Jens Riboe
Senior/Expert Software Developer
Så här använder du span-view i C++

C++ har traditionellt erbjudit flera sätt att representera en sekvens av element: pekare, rå-arrayer och olika STL-containrar. I C++20 tillkom std::span, och i C++23 byggdes konceptet ut till std::mdspan. Dessa är icke-ägande vyer över data som förenklar kod, minskar risken för buggar, och förbättrar prestanda.

std::span<T>

En std::span är en lättviktig vy över en sekvens av element av typen T. Den äger inte minnet, utan fungerar som ett “fönster” mot befintlig data. Så här ser definitionen ut (förenklad):

template<typename T, std::size_t Extent = std::dynamic_extent>
class span;
  • T är elementtypen.
  • Extent anger längden vid kompilering, eller std::dynamic_extent (standard) om längden bestäms vid körning.

Egenskaper

  • Äger inte data, bara en pekare och ett antal element.
  • Säker: håller reda på längden, så man undviker buffer overflow.
  • Kan skapas från arrayer, std::vector, std::array, eller pekare + längd.
  • Ger bekväm iteration, slicing och interoperabilitet med modern C++.

Exempel

#include <span>
#include <vector>
#include <array>
#include <list>
#include <print>
#include <string>
using std::string;

void process(string const& what, std::span<int const> s) {
    std::print("{:23}: [", what);
    for (std::size_t i = 0; i < s.size(); ++i) {
        std::print("{}", s[i]);
        if (i + 1 != s.size()) std::print(", ");
    }
    std::println("]");
}

int main() {
    // 1) std::vector
    auto v = std::vector<int>{1, 2, 3, 4, 5};
    process("std::vector", v); // implicit konvertering till std::span<const int>

    // 2) Slice av std::vector (första tre elementen)
    process("std::vector slice", std::span{v}.first(3));
    process("std::vector slice (alt)", std::span{v}.subspan(2, 3));

    // Alternativ: std::array
    auto sa = std::array<int, 3>{11, 12, 13};
    process("std::array", sa);

    // 3) Statisk array (C-array) – implicit konvertering till span
    int a[] = {7, 8, 9, 10};
    process("native static array", a);

    // 4) Dynamisk array (new[]), skapa span från pekare + längd
    const int n = 4;
    auto* dyn = new int[n]{20, 21, 22, 23};
    process("native dynamic array", {dyn, static_cast<std::size_t>(n)});
    delete[] dyn;

    //auto lst = std::list<int>{90, 91, 92};
    //process("std::list", lst);
    //error: could not convert ‘lst’ from ‘std::__cxx11::list<int>’ to ‘std::span<const int>’
}
std::vector            : [1, 2, 3, 4, 5]
std::vector slice      : [1, 2, 3]
std::vector slice (alt): [3, 4, 5]
std::array             : [11, 12, 13]
native static array    : [7, 8, 9, 10]
native dynamic array   : [20, 21, 22, 23]

Process finished with exit code 0

Nyttiga metoder

  • size(), empty()
  • front(), back(), operator[]
  • subspan(offset, count) – skapa ett nytt span över delmängd

Krav på underliggande container

Eftersom std::span inte äger sitt data, så måste källan som span pekar på uppfylla vissa krav:

  • Data måste vara lagrad i ett minnesblock (contiguous).
  • Typiska kandidater är: C-array:er, std::array, std::vector.
  • Det fungerar inte med länkade datastrukturer såsom std::list eller std::forward_list.
  • Konstruktionen kräver antingen [pekare, längd] eller en container som erbjuder data() + size().

Detta gör att std::span är mycket flexibel men samtidigt tydlig i sitt användningsområde: den är alltid en vy över ett sammanhängande block-minne.


std::mdspan<T, Extents, LayoutPolicy, AccessorPolicy>

När std::span är endimensionell, så introducerar std::mdspan (C++23) möjligheten till flerdimensionella vyer. Den är särskilt användbar i numeriska beräkningar, bildbehandling och maskininlärning.

I skrivande stund finns det ingen kompilator-version som har implementerat mdspan, så denna artikel-del nedan kan innehålla felaktigheter, eftersom jag inte kunnat test-köra exemplen för mdspan 😠

Definition (förenklad):

template<
    typename T,
    typename Extents,
    typename LayoutPolicy = std::layout_right,
    typename AccessorPolicy = std::default_accessor<T>
> class mdspan;
  • T – typen på elementen.
  • Extents – beskriver dimensionernas storlek (std::extents).
  • LayoutPolicy – minneslayout (row-major, column-major, eller egen).
  • AccessorPolicy – hur man läser/skriv data (kan anpassas).

Exempel: 2D matris

#include <mdspan>
#include <vector>
#include <print>

int main() {
    constexpr auto rows = 3, cols = 4;
    auto data = std::vector<int>(rows * cols);
    for (int i = 0; i < data.size(); ++i) data[i] = i + 1; // fyll med 1..12

    auto m = std::mdspan(data.data(), rows, cols);
    for (int r = 0; r < rows; ++r) {
        for (int c = 0; c < cols; ++c) {
            std::print("{:3}", m(r, c));
        }
        std::println("");
    }
}

Utskrift:

  1  2  3  4
  5  6  7  8
  9 10 11 12

LayoutPolicy

  • std::layout_right (default) → C-liknande row-major.
  • std::layout_left → Fortran-liknande column-major.
  • Egen layout är möjlig för avancerade användningsfall.

Exempel: Fortran-liknande layout (column-major)

#include <mdspan>
#include <vector>
#include <print>

int main() {
    constexpr auto rows = 3, cols = 4;
    auto data = std::vector<int>(rows * cols);
    for (int i = 0; i < data.size(); ++i) data[i] = i + 1;

    // Column-major istället för row-major
    auto m = std::mdspan<int, std::extents<3,4>, std::layout_left>(data.data());
    for (int r = 0; r < rows; ++r) {
        for (int c = 0; c < cols; ++c) {
            std::print("{:3}", m(r, c));
        }
        std::println("");
    }
}

Här lagras elementen kolumnvis i minnet, vilket kan vara fördelaktigt om man arbetar med algoritmer eller bibliotek som förväntar sig Fortran-liknande layout.

Utskrift:

  1  4  7 10
  2  5  8 11
  3  6  9 12

AccessorPolicy

AccessorPolicy styr hur element hämtas (och ev. skrivs) från den underliggande datan. Standard är std::default_accessor<T>, som i praktiken gör ett vanligt pekarindex (*(p + i)). En egen accessor kan bland annat användas för att:

  • göra read-only-vyer över icke-konst data,
  • lägga till bounds-checking eller andra kontrakt i debugbyggen,
  • implementera alternativa representationer (t.ex. komprimerade data) med ett proxy-objekt.

En accessor måste ange minst följande medlemmar:

  • element_type – elementtyp (ska matcha mdspan's ElementType),
  • reference – referenstypen som returneras av access(...),
  • data_handle_type – typen på datahandtaget (ofta en pekare),
  • offset_policy – en accessor-typ som kan användas för offset:ade vyer (ofta samma typ),
  • access(data_handle_type, index) – returnerar ett element (som reference),
  • offset(data_handle_type, i) – flyttar datahandtaget i steg framåt (t.ex. p + i).

Obs: AccessorPolicy påverkar åtkomst, inte minneslayout. Layout kontrolleras av LayoutPolicy.

Ett litet (torr-simmat) exempel nedan visar en accessor som returnerar element som read-only.

#include <mdspan>
#include <vector>
#include <print>
#include <cmath>

// Minimal read-only accessor
struct ro_accessor {
    using element_type     = const int;          // matchar mdspan's ElementType
    using reference        = const int&;         // vad access() returnerar
    using data_handle_type = const int*;         // handtag till data
    using offset_policy    = ro_accessor;        // samma typ duger för offset

    constexpr reference access(data_handle_type base, std::size_t index) const noexcept {
        return base[index];
    }
    constexpr data_handle_type offset(data_handle_type base, std::size_t index) const noexcept {
        return base + index;
    }
};

int main() {
    constexpr auto rows = 2, cols = 3;
    auto data = std::vector<int>{1,2,3,4,5,6};

    // Vanlig läsbar vy (default accessor över const data)
    auto m_def = std::mdspan<const int, std::extents<rows, cols>>( data.data() );

    // Read-only vy via explicit accessor-policy
    auto m_ro = std::mdspan<const int, 
                            std::extents<rows, cols>, 
                            std::layout_right, ro_accessor>( data.data() );

    // Läsning fungerar
    for (int r = 0; r < rows; ++r) {
        for (int c = 0; c < cols; ++c) {
            std::print("{:3}", m_ro(r, c));
        }
        std::println("");
    }

    // Skrivförsök (m_ro(r,c) = 42;) kompilerar inte, eftersom reference är const int&
}

Utskrift:

  1  2  3
  4  5  6

Prestanda och användningsområden

Både std::span och std::mdspan är designade för nollkostnadsabstraktioner (zero-const abstractions): de ska vara lika snabba som att arbeta direkt med pekare och index.

TypDimensionerÄgandeVanlig användning
std::span1DNejFunktionparametrar, slicing, vyer
std::mdspan1D, 2D, …NejMatrisberäkningar, bilddata, tensorer

Sammanfattning

  • std::span (C++20) ger ett säkert och smidigt sätt att arbeta med sekvenser utan att kopiera data.
  • std::mdspan (C++23) utökar konceptet till flera dimensioner, med stöd för olika minneslayouter.
  • Båda är icke-ägande vyer som minskar buggar och ökar prestanda i modern C++.

När du skriver generiska funktioner som arbetar på data, välj span eller mdspan istället för råpekare — det gör din kod både robustare och mer uttrycksfull.