CodeGym /Kurslar /C++ SELF /std::span kimi davamlı verilənlərə view

std::span kimi davamlı verilənlərə view

C++ SELF
Səviyyə, Dərs
Mövcuddur

1. Əsaslar: std::span: yaddaş, view və const-kontrakt

Əgər tədris proqramları yazırsınızsa, əvvəl hər şey sadə görünür: məsələn, mənim elimdə std::vector<int> var, onu funksiyaya ötürürəm və hər şey yaxşıdır. Amma sonra kodda gözlənilmədən adi C-massiv peyda olur, sonra std::array, və siz bütününü deyil, məsələn yalnız ilk 10 elementi emal etmək istəyə bilərsiniz. Və klassika başlayır: ya eyni funksiyanın üç versiyasını yazırsınız, ya da “göstərici + ölçü” üsuluna keçir və parametrəri qarışdırmayın rejimində yaşamağa başlayırsınız.

std::span — “göstərici + uzunluq” ideyasının normal bir tip şəklində, metodları və aydın kontraktı olan standart qablaşdırmasıdır. O məhz standart kitabxananın vokabulyar tipi kimi mövcuddur: yəni interfeyslərdə istifadə etdiyiniz tip ki, digər insanlar (və ya bir həftə sonra siz) dərhal mənasını başa düşsünlər. span-ın standart sənədlərdə geniş müzakirə olunması və tənzimlənməsi onun dil daxilində “rəsmi” statusunu göstərir.

Davamlı verilənlər nədir: sadə yaddaş modeli

std::span-ı götürməzdən əvvəl onun nəyə baxa biləcəyini başa düşmək lazımdır. “Davamlı verilənlər” elementlərin yaddaşda boşluqsuz yan-yana yerləşməsini ifadə edir: biri digərinin ardınca, kinoteatrdakı yerlər kimi (amma kinoteatrda bəzən yerlər sıradan çıxa bilər, yaddaşda isə — əgər siz özünüz UB etməsəniz — belə olmur).

Belə konteynerlərə C-massiv T arr[N], std::array<T, N>, std::vector<T> (onun elementləri ardıcıl yerləşir, vektor böyüyərkən yaddaş “köçürülmədiyi” müddətcə) və std::string (simvollar da ardıcıl yerləşir və onlar üçün data() və size() mövcuddur). Məsələn, std::list davamlı deyil: orada hər element ayrıca yaşayır, elə bil pişiklər kimi yan-yana yatmaq istəmirlər.

Aşağıda “ardıcıl” nə demək olduğunu təsəvvür etmək üçün nümunə sxem var:

flowchart LR
    A["arr[0]"] --> B["arr[1]"] --> C["arr[2]"] --> D["arr[3]"]
    subgraph Memory["Yaddaş: ardıcıl, deşiksiz"]
      A
      B
      C
      D
    end

Və məhz belə “sıx sıra” üçün std::span nəzərdə tutulub.

std::span<T>: “göstərici + ölçü”, amma insan üçün

İndi mühazirənin qəhrəmanı ilə tanış ola bilərik. std::span<T> — davamlı yerləşən T elementlər üzrə sahibliyə malik olmayan görünüş (view)-dür. Daxildə bu həqiqətən də (T*, size) cütünə çox yaxındır, amma normal interfeysə malikdir: size(), empty(), operator[], range-for və s.

Fəlsəfəni qarışdırmamaq vacibdir. std::vector<int> — bu ədəd dolu və öz saxlama sahəsinə sahib qutudur. std::span<int> — qutunun üzərində yapışdırılmış stiker kimidir: “elementlərə buradan və bu qədər qədər bax”. Əgər qutu yox olarsa, stiker qəflətən ədədləri cibində saxlamayacaq.

“span yaratdıq və ona baxdıq” üçün minimal nümunə:

#include <iostream>
#include <span>

int main() {
    int a[] = {10, 20, 30};

    std::span<int> s(a); // bütün massivə view
    std::cout << s.size() << '\n';   // 3
    std::cout << s[1] << '\n';       // 20
}

Diqqət yetirin: span tez-tez obyekt üzrə ötürülür, çünki o kiçikdir (iki ədəd: ünvan və uzunluq kimi). const std::span<int>& ötürmək adətən lazım deyil: burada “nəsə böyük” kopyalamaq kimi qənaət etmirsiniz, böyük olacaq heç nə yoxdur.

std::span<const T> vs std::span<T>: “oxumaq” və “elementləri dəyişmək”

Yeni başlayanlar span-ı ilk dəfə gördükdə tez-tez eyni şeyi edirlər: hər yerdə std::span<int> yazırlar, hətta funksiya yalnız oxuyursa. Bu, bəzən çörək kəsmək üçün biçənəkdən istifadə etmək kimidir: işləyə bilər, amma ətrafdakılar üçün narahatlıq yaradır.

Funksiya elementləri dəyişməməlidirsə, doğru seçim — std::span<const T>-dir. Bu tip parametrə “mən oxuyuram, dəyişdirmirəm” vədini dərhal “tikir”.

İki funksiyanı müqayisə edin (kod demək olar ki eynidir — fərq məhz kontraktdadır):

#include <span>

int sum(std::span<const int> xs) {
    int total = 0;
    for (int x : xs) total += x;
    return total;
}
#include <span>

void negate_all(std::span<int> xs) {
    for (int& x : xs) x = -x;
}

Sadə məna budur: std::span<int> elementləri dəyişməyə imkan verir, amma yenə də ölçünü dəyişməyə icazə vermir (span-a element əlavə etmək olmaz, çünki əlavə etmə yalnız sahibi, məsələn vector, tərəfindən mümkündür).

3. Hardan std::span almaq olar

İndi xoş hissə gəlir: span həqiqətən kodu “üniversal” edir, amma sehrsiz və şablonsuz (hələlik). Siz sadəcə davamlı verilənləri götürür və onlara view yaradırsınız.

C-massivdən

Əvvəlcə başa düşmək vacibdir: C-massiv doğrudan davamlı blokdur. std::span ondan çox rahat şəkildə yaradılır:

#include <iostream>
#include <span>

void print_first(std::span<const int> xs) {
    if (xs.empty()) return;
    std::cout << xs[0] << '\n';
}

int main() {
    int a[] = {5, 6, 7};
    print_first(a); // span qeyri-aydın şəkildə yaradılır
}

Burada faydalı məqam belədir: print_first “massiv” adlı xüsusi saxlama növünü tanımır. O yalnız bilir: “mənə int ardıcıllığı verildi”.

std::array-dan

std::array — “mədəni” massivdir: ölçü tipdə saxlanılır, metodları var və o alqoritmlərlə yaxşı dostluq edir. span üçün o da əla mənbədir:

#include <array>
#include <iostream>
#include <span>

int main() {
    std::array<int, 4> a{1, 2, 3, 4};
    std::span<const int> s(a);

    std::cout << s.size() << '\n'; // 4
}

std::vector-dən

Vektor da ideal şəkildə uyğundur, amma bir xəbərdarlıqla: vektorun yaddaşı böyüdükdə yenidən ayıla bilər. Bu faktı burada qəbul edirik; “view nə vaxt pozulur” mövzusunu növbəti mühazirədə daha ətraflı müzakirə edəcəyik. Hazırda span-ı çağırış ətrafında qısaömürlü obyekt kimi istifadə edin.

#include <iostream>
#include <span>
#include <vector>

int sum(std::span<const int> xs) {
    int total = 0;
    for (int x : xs) total += x;
    return total;
}

int main() {
    std::vector<int> v{10, 20, 30};
    std::cout << sum(v) << '\n'; // 60
}

Oxunaqlıq baxımından bu ən xoş məqamlardan biridir: funksiya “vektor” yox, “elementlər diapazonu” qəbul edir. Və qəflətən ona həm massiv, həm std::array, həm də std::vector verə bilərsiniz.

std::string üçün std::span: bəli, string davamlıdır, amma bu string_view deyil

İncə məqama keçirik: “string üzərinə span”. std::string-in simvolları ardıcıl yerləşir, ona görə std::span<const char> yaradıb stringi simvol massiv kimi emal etmək olar. Bu bəzən faydalıdır, əgər siz məhz bayt və ya simvol təmsilatı ilə işləmək istəyirsinizsə və string metodlarının xüsusiyyətləri lazım deyil.

Amma rolları qarışdırmamaq vacibdir. std::string_view məhz string-ə uyğun view-dir (orada find, substr, remove_prefix kimi əlverişli metodlar var). std::span<const char> isə simvolları massiv elementləri kimi görən view-dir. O substring əməliyyatlarını “başa düşmür”, amma char diapazonu kimi universal davranır.

Nümunə: mətn daxilində neçə rəqəm olduğunu sayaq (çox ağıllı deyil, amma nümayiş üçündür):

#include <iostream>
#include <span>
#include <string>

int count_digits(std::span<const char> chars) {
    int c = 0;
    for (char ch : chars) {
        if (ch >= '0' && ch <= '9') ++c;
    }
    return c;
}

int main() {
    std::string s = "Room 101";
    std::cout << count_digits(std::span<const char>(s.data(), s.size())) << '\n'; // 3
}

Niyə sadəcə count_digits(s) yox? Çünki std::string avtomatik span-a çevrilməyə borclu deyil. Biz data() və size() vasitəsilə explicit şəkildə span yaradırıq və bu yaxşıdır: beləliklə yanlışlıqla istəmədiyiniz yerə view almaq ehtimalı azalır.

4. Gündəlik əməliyyatlar ilə span

İndi eniş etmək üçün faydalıdır: halların 90%-də span-dan çox sadə əməliyyatlar lazım olur. Siz empty()-i yoxlayırsınız, size()-i öyrənirsiniz, range-for ilə keçirsiniz, bəzən indekslə element alırsınız. Əslində bu, std::vector və std::string üçün etdiklərinizlə eynidir, sadəcə indi bu “başqalarının məlumatları” üçün işləyir.

“Diapazonu gözəl çap etmək” üçün kiçik nümunə:

#include <iostream>
#include <span>

void print(std::span<const int> xs) {
    std::cout << '[';
    for (std::size_t i = 0; i < xs.size(); ++i) {
        std::cout << xs[i] << (i + 1 == xs.size() ? "" : ", ");
    }
    std::cout << "]\n";
}

int main() {
    int a[] = {3, 1, 4};
    print(a); // [3, 1, 4]
}

Diqqət yetirin: span-ın indeksləşdirməsi massiv və ya vector-dəki operator[] kimi “sığortasızdır”. Buna görə indeks xaricdən gəlirsə, yoxlama sizin məsuliyyətinizdir.

subspan(): kopiyalama olmadan məlumatın hissəsinə “pəncərə”

İndi ən ləzzətli hissə: std::span kopiyalamadan “alt-diapazon” yaratmağa imkan verir. Bu mənada std::string_view::substr()-a oxşardır: yeni massiv yaratmırsınız, köhnə massivdən bir parça üçün yeni view yaradırsınız.

Bu praktik tapşırıqlarda daim faydalıdır. Məsələn, “ilk elementi çıxmaqla bütün elementləri emal et” və ya “ilk N” və ya “orta hissə” kimi operasiyalar üçün. Yeni std::vector-a kopiyalama etmək əvəzinə subspan() istifadə edirsiniz.

Nümunə: orta iki elementi götürürük:

#include <iostream>
#include <span>

int main() {
    int a[] = {10, 20, 30, 40};

    std::span<int> all(a);
    std::span<int> mid = all.subspan(1, 2); // {20, 30}

    std::cout << mid[0] << ' ' << mid[1] << '\n'; // 20 30
}

Vacibdir: subspan() sahibi dəyişmir və məlumatları köçürmür. O yalnız deyir: “offset mövqeyindən başlayaraq count elementə bax”. Əgər səhv sərhədlər göstərsəniz — məntiq xətası (və bəzən UB) alacaqsınız, eynilə istənilən indeksləşdirmədə olduğu kimi.

5. Praktik nümunə: “MiniStats”-a span əlavə edirik

std::span-ın “ağıllı nəzəriyyə” olaraq qalmasına yol verməmək üçün onu kursumuzun kiçik bir konsol tətbiqinə daxil edək. Gəlin “MiniStats” modulunu yaradaq: o ardıcıllıq üzrə cəmi və ortanı hesablamağı bacarır və məlumat mənbəyi — massiv, std::array və ya std::vector — əhəmiyyət kəsb etmir.

span<const int> üzrə cəm və orta

İki funksiyanı edək: mümkün qədər sadə və “gözlə oxunan”. Hələ mürəkkəb modelə toxunmuruq: sadəcə ədədlərlə və çıxışla işləyirik.

#include <iostream>
#include <span>

int sum(std::span<const int> xs) {
    int total = 0;
    for (int x : xs) total += x;
    return total;
}

double average(std::span<const int> xs) {
    if (xs.empty()) return 0.0;
    return static_cast<double>(sum(xs)) / xs.size();
}

Burada xoşdur ki, average istənilən davamlı ədədləri qəbul edə bilər: ona vector lazım deyil. Və boş diapazonu dürüstcə emal edir.

Həm massiv, həm vektor üçün bu funksiyaları çağırırıq

İndi “bütün bu işə niyə ehtiyac var” göstərək:

#include <iostream>
#include <span>
#include <vector>

int sum(std::span<const int> xs);
double average(std::span<const int> xs);

int main() {
    int a[] = {1, 2, 3};
    std::vector<int> v{10, 20, 30, 40};

    std::cout << sum(a) << '\n';        // 6
    std::cout << average(v) << '\n';    // 25
}

Fokus ondadır ki, eyni funksiyalar müxtəlif məlumat mənbələri ilə işləyir. Və onların imzaları yalan demir: sum və average ədədlərə sahib deyil və onları dəyişmir — sadəcə oxuyurlar.

subspan() ilə yalnız bir hissəni emal edirik

Real proqramlarda çox tipik ssenari: “statistika bütün ədədlər üzrə yox, quyruq/önkimi üzrə hesablanmalıdır”. Göstərək:

#include <iostream>
#include <span>
#include <vector>

int sum(std::span<const int> xs);

int main() {
    std::vector<int> v{5, 100, 100, 100};

    std::span<const int> all(v);
    std::span<const int> tail = all.subspan(1); // ilk elementi çıxmaqla hamısı

    std::cout << sum(tail) << '\n'; // 300
}

Diqqət yetirin: biz {100, 100, 100} elementlərini yeni konteynerə kopyalamadıq. Sadəcə lazım olan diapazon üçün “pəncərə” yaratdıq.

6. std::span ilə iş zamanı tipik səhvlər

Səhv №1: span-ı konteyner kimi qəbul edib “element əlavə etmək” cəhdi.
std::span sahib deyil, onun push_back-ı yoxdur və resize edə bilməz. Əgər özünüzü “bura bir element daha əlavə edim” fikrində tutdunuzsa, demək sizə sahibli konteyner (std::vector) lazımdır və ya sahibi dəyişməlisiniz, view yox.

Səhv №2: std::span<T> qəbul etmək, halbuki funksiya heç nəyi dəyişmir.
Belə kod kompilyasiya olunar, amma interfeys daha riskli olur: çağıra bilən kod indi funksiyanın verilənləri dəyişə biləcəyini düşünür. Doğru vərdiş: default olaraq std::span<const T> qəbul etmək, std::span<T>-dən yalnız elementləri dəyişmək kontraktın bir hissəsidirsə istifadə etmək.

Səhv №3: span yaratmaq və onu “uzun müddət saxlamaq”, sahib haqqında düşünməmək.
span verilənlərin ömrünü uzatmır. Əgər sahibi yox olarsa (məsələn, bu funksiya daxilində lokal std::vector idisə), span boş ünvana malik kağız parçasına çevrilir. Eyni dərəcədə pisdir sahibin “köçürülməsi” (xüsusilə std::vector böyüdükdə). Bu günlük qaydı yadda saxlayın: span qısaömürlü və istifadə yerinə yaxın yaşamalıdır.

Səhv №4: unutmaq ki, operator[] sərhədləri yoxlayır.
span-ın indeksləşdirməsi massiv və vector-in []-ilə eynidir — sığortasız. Əgər indeks hesablanır və ya kənardan gəlirsə, əvvəlcə i < xs.size() yoxlayın. Əks halda ya natamam məlumat, ya düşmə, ya da (ən “əyləncəli”si) “bəzən işləyir” vəziyyəti əldə edəcəksiniz.

Səhv №5: std::string-ə span etmək və “string əməliyyatları” gözləmək.
std::span<const char> char diapazonudur, string deyil. O find, starts_with və substring kimi əməliyyatları bilmir. Əgər vəzifə mətnlə bağlıdırsa — çox vaxt std::string_view daha uyğundur. Əgər vəzifə bayt/simvol massivinə aiddirsə — onda span layiqli seçimdir.

1
Tapşırıq
C++ SELF, səviyyə, dərs
Bağlanıb
Səbətin yekunu
Səbətin yekunu
1
Tapşırıq
C++ SELF, səviyyə, dərs
Bağlanıb
Seqmentin işarəsinin inversiyası
Seqmentin işarəsinin inversiyası
1
Tapşırıq
C++ SELF, səviyyə, dərs
Bağlanıb
İlkərin ortası
İlkərin ortası
1
Tapşırıq
C++ SELF, səviyyə, dərs
Bağlanıb
Simvolların təhlili
Simvolların təhlili
Şərhlər
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION