Форум программистов, компьютерный форум, киберфорум
NullReferenced
Войти
Регистрация
Восстановить пароль

std::span в C++: Введение в невладеющее представление

Запись от NullReferenced размещена 18.03.2025 в 20:50
Показов 1418 Комментарии 0
Метки c++, c++20, std::span

Нажмите на изображение для увеличения
Название: 9da2ed95-f2c6-4369-ae08-df4084222522.jpg
Просмотров: 62
Размер:	197.6 Кб
ID:	10450
С появлением стандарта C++20 у нас появился новый инструмент — std::span, который представляет собой невладеющее представление для работы с последовательностями данных.

std::span — это легковесный объект, который предоставляет доступ к непрерывной последовательности элементов без владения ими. По сути, это "взгляд" на существующие данные, который не копирует и не управляет памятью этих данных. Такой подход позволяет решить ряд классических проблем при работе с массивами в C++ и предоставляет более безопасную альтернативу обычным указателям.

Проблема, которую решает std::span, хорошо известна опытным разработчикам на C++. При передаче массива в функцию традиционно приходилось передавать отдельно указатель на первый элемент и размер массива:

C++
1
2
3
4
5
void processArray(int* data, size_t size) {
    for (size_t i = 0; i < size; ++i) {
        // Обработка элементов
    }
}
Этот подход имеет ряд недостатков. Он не типобезопасен, легко допустить ошибку, передав неправильный размер или указатель, и к тому же функция не может принимать различные типы контейнеров без перегрузки. С помощью std::span эта проблема решается просто:

C++
1
2
3
4
5
void processArray(std::span<int> data) {
    for (auto elem : data) {
        // Обработка элементов
    }
}
Теперь функция может принимать любой последовательный контейнер элементов типа int — будь то обычный C-массив, std::vector, std::array или даже часть другого контейнера.

История появления std::span тесно связана с эволюцией стандарта C++. До C++20 разработчики часто создавали собственные классы-обертки для работы с массивами или использовали библиотеки вроде Boost, которые предоставляли похожую функциональность. Например, в библиотеке GSL (Guidelines Support Library) существовал класс span, который стал прообразом стандартного std::span.

Идея std::span была предложена как часть усилий по повышению безопасности и удобства работы с памятью в C++. Первоначальный проект был разработан на основе концепций, представленных в "C++ Core Guidelines" — наборе рекомендаций по написанию чистого, безопасного и эффективного кода на C++. Эти рекомендации, соавторами которых являются Бьярне Страуструп и Херб Саттер, предлагали использовать "невладеющие представления" вместо голых указателей для повышения безопасности. После тщательного обсуждения и доработки std::span был включен в стандарт C++20, став одним из важных дополнений к стандартной библиотеке. Он предлагает сбалансированное решение, которое сочетает в себе безопасность, эффективность и гибкость.

Чтобы понять значимость std::span, стоит взглянуть на то, какие проблемы он решает на практике. Представьте ситуацию, когда вам нужно написать функцию, которая обрабатывает часть массива или вектора. Без std::span вам придётся передавать указатели и размеры, что чревато ошибками:

C++
1
2
3
4
void processPart(int* start, size_t count);
 
std::vector<int> vec = {1, 2, 3, 4, 5};
processPart(&vec[1], 3); // Обработка элементов 2, 3, 4
С std::span тот же код может быть переписан более безопасно и элегантно:

C++
1
2
3
4
void processPart(std::span<int> data);
 
std::vector<int> vec = {1, 2, 3, 4, 5};
processPart(std::span(vec).subspan(1, 3)); // Явно создаем подмножество
При этом std::span не только делает код более читаемым, но и предотвращает ряд потенциальных ошибок, связанных с управлением памятью и границами массивов.

Важно отметить, что std::span — это не панацея и не полная замена всех существующих контейнеров. Это скорее дополнительный инструмент, который имеет свою специфическую область применения. В первую очередь, std::span полезен там, где нужно работать с непрерывными последовательностями данных без копирования и с минимальными накладными расходами.

Основные концепции



Ключевая особенность std::span заключается в том, что это невладеющее представление для непрерывной последовательности элементов. Но что именно означает "невладеющее"? В контексте программирования на C++ это означает, что std::span не выделяет память для хранения элементов и не отвечает за её освобождение. Он просто предоставляет интерфейс для доступа к существующим данным. По своей сути std::span похож на пару из указателя и размера, но с рядом дополнительных преимуществ. Внутренняя реализация std::span действительно содержит указатель на первый элемент и количество элементов в последовательности. Однако в отличие от прямого использования указателей, std::span предоставляет безопасный и удобный интерфейс для работы с данными.

C++
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
template <class T, size_t Extent = dynamic_extent>
class span {
public:
    // Типы и константы
    using element_type = T;
    using value_type = typename std::remove_cv<T>::type;
    using size_type = std::size_t;
    using difference_type = std::ptrdiff_t;
    using pointer = T*;
    using const_pointer = const T*;
    using reference = T&;
    using const_reference = const T&;
    using iterator = /* implementation-defined */;
    // ...
};
Сравним std::span с другими способами работы с последовательностями данных в C++.

Указатели и массивы:
C++
1
2
3
4
5
void process(int* array, size_t size) {
    for (size_t i = 0; i < size; ++i) {
        array[i] *= 2;
    }
}
При использовании простых указателей мы сталкиваемся с несколькими проблемами:
  • Нет автоматической проверки границ.
  • Необходимо передавать размер отдельно.
  • Легко передать неправильный размер или указатель.

std::vector:
C++
1
2
3
4
5
void process(std::vector<int>& vec) {
    for (auto& val : vec) {
        val *= 2;
    }
}
std::vector решает проблемы с безопасностью, но имеет свои минусы:
  • Если функция получает копию вектора, это приводит к копированию всех данных.
  • Не может принимать другие типы контейнеров без перегрузок.
  • Избыточен, когда нужен только доступ к данным без владения.

std::span:
C++
1
2
3
4
5
void process(std::span<int> data) {
    for (auto& val : data) {
        val *= 2;
    }
}
std::span объединяет лучшие качества обоих подходов:
  • Безопасный доступ к элементам.
  • Не требует копирования данных.
  • Может работать с любым типом контейнера, который хранит элементы в непрерывной памяти.
  • Размер является частью объекта, что устраняет ошибки с неправильным размером.

Одно из главных преимуществ std::span с точки зрения безопасности — это встроенная информация о размере. Когда размер является неотъемлемой частью объекта, снижается вероятность выхода за границы массива. Кроме того, std::span предоставляет методы, которые выполняют проверку границ во время выполнения, например, метод at().

C++
1
2
3
4
5
6
7
8
9
std::vector<int> numbers = {1, 2, 3, 4, 5};
std::span<int> span_numbers(numbers);
 
// Безопасный доступ с проверкой границ
try {
    int value = span_numbers.at(10); // Выбросит исключение
} catch (const std::out_of_range& e) {
    // Обработка исключения
}
Важная особенность std::span — это возможность работы с константными данными через std::span<const T>. Это позволяет четко выразить намерение, что данные не должны изменяться через этот span. Такой подход улучшает const-корректность кода и помогает предотвратить случайные модификации.

C++
1
2
3
4
5
6
7
8
9
10
11
12
void display(std::span<const int> data) {
    for (int val : data) {
        std::cout << val << " ";
    }
    std::cout << std::endl;
}
 
std::vector<int> mutable_data = {1, 2, 3};
std::span<const int> const_view(mutable_data);
 
// Следующая строка вызвала бы ошибку компиляции
// const_view[0] = 42;
Использование std::span<const T> особенно полезно при передаче данных в функции, которые не должны изменять эти данные. Это делает код более понятным и безопасным.

В каких случаях стоит использовать std::span? Хотя универсальных правил не существует, можно выделить несколько типичных сценариев:
1. При передаче массивов или частей массивов в функции: std::span позволяет единообразно работать с разными типами контейнеров, не беспокоясь о копировании данных.
2. При работе с подмножествами данных: Когда нужно оперировать только частью большого массива, std::span позволяет создать "окно" в данные без их копирования.
3. В алгоритмах, которые не должны владеть данными: Например, в функциях сортировки, поиска или фильтрации, которые просто обрабатывают существующие данные.
4. Для улучшения const-корректности: Когда функции нужен только доступ для чтения, использование std::span<const T> делает это намерение явным.
5. При работе с API, которое принимает указатель и размер: std::span можно использовать для обертывания таких вызовов, делая код более безопасным.

Стоит отметить, что std::span — не замена для всех контейнеров. Его ниша — это невладеющий доступ к непрерывным последовательностям данных. Если вам нужно хранение с управлением жизненным циклом данных, лучше использовать std::vector или std::array. Кроме того, важно помнить, что std::span не удлиняет время жизни объектов, на которые он ссылается. Программист должен убедиться, что данные остаются действительными в течение всего времени использования span. Например, следующий код приведет к неопределенному поведению:

C++
1
2
3
4
std::span<int> createSpan() {
    std::vector<int> temp = {1, 2, 3};
    return std::span<int>(temp); // ОПАСНО! temp разрушается при выходе из функции
}
Еще одна важная концепция — это возможность работы с фиксированным или динамическим размером. Шаблонный класс std::span принимает два параметра: тип элемента T и "протяженность" (extent) Extent, которая может быть либо числом, известным на этапе компиляции, либо специальным значением std::dynamic_extent.

C++
1
2
3
4
5
// Span с динамическим размером
std::span<int> dynamicSpan;
 
// Span с фиксированным размером (5 элементов)
std::span<int, 5> fixedSpan;
Span с фиксированным размером проверяет соответствие размера контейнера заявленному размеру на этапе компиляции, что добавляет дополнительный уровень безопасности. Кроме того, для span с фиксированным размером компилятор может генерировать более оптимизированный код, так как размер известен заранее.

C++
1
2
3
int arr[5] = {1, 2, 3, 4, 5};
std::span<int, 5> fixedSpan(arr);  // Правильно
// std::span<int, 10> wrongSpan(arr);  // Ошибка компиляции!
Однако в большинстве случаев предпочтительнее использовать span с динамическим размером, так как он более гибкий и может работать с контейнерами произвольного размера.

Взаимодействие std::span с существующим кодом также заслуживает внимания. Если у вас есть функции, которые принимают указатель и размер, вы можете легко адаптировать их для работы с std::span:

C++
1
2
3
4
5
6
7
8
9
// Старый код
void oldFunction(int* data, size_t size) {
    // ...
}
 
// Адаптер для использования со span
void newFunction(std::span<int> data) {
    oldFunction(data.data(), data.size());
}
Такой подход позволяет постепенно модернизировать кодовую базу, добавляя преимущества безопасности и удобства, которые предоставляет std::span. Важно также понимать разницу между std::span и другими "представлениями" в стандартной библиотеке, такими как std::string_view (введен в C++17). Обе эти сущности предоставляют невладеющий доступ к последовательностям данных, но std::string_view специализирован для работы со строками, в то время как std::span — более общая абстракция для любых последовательных данных.

C++
1
2
3
std::string s = "Hello, world!";
std::string_view sv(s);  // Невладеющее представление строки
std::span<const char> sp(s);  // Невладеющее представление последовательности символов
Необходимо помнить, что std::span требует, чтобы данные хранились в непрерывной памяти. Это означает, что не все контейнеры стандартной библиотеки могут быть использованы со std::span. Контейнеры, которые гарантируют непрерывное хранение, включают:
  • Обычные массивы (T[]),
  • std::array,
  • std::vector,
  • std::string (как последовательность символов).

Контейнеры, которые не гарантируют непрерывное хранение (например, std::list, std::map, std::set), не могут быть напрямую использованы со std::span. Стоит также отметить, что std::span может быть использован для более удобной работы с многомерными массивами. Например, для двумерного массива можно создать span из spans:

C++
1
2
int matrix[3][4] = {{1, 2, 3, 4}, {5, 6, 7, 8}, {9, 10, 11, 12}};
std::span<std::span<int, 4>, 3> matrix_span(reinterpret_cast<std::span<int, 4>(&matrix[0])[0], 3);
Однако такой подход требует осторожности и хорошего понимания разметки памяти.
При работе с std::span также важно учитывать, что он не увеличивает время жизни данных, на которые ссылается. Это особенно важно при работе с временными объектами или при передаче span между функциями. Использование span, указывающего на недействительные данные, приводит к неопределенному поведению.

C++
1
2
3
4
std::span<int> dangerous() {
    std::vector<int> local = {1, 2, 3};
    return std::span<int>(local);  // local уничтожается при выходе из функции!
}
Этот код приведет к проблемам, поскольку std::span будет ссылаться на память, которая уже освобождена.

std::pair<std::list<std::pair< >>::iterator, > ломается при возврате из функции
#include &lt;iostream&gt; #include &lt;list&gt; #include &lt;string&gt; #include &lt;utility&gt; using lp = std::list&lt;std::pair&lt;std::string, int&gt;&gt;; auto f(lp...

Не могу разобраться как обновить в std::map<std::string, вектор_структур>
Не могу разобраться как обновить вектор структур после его добавления в map без удаления и перезаписи struct pStruct { int a; ...

Не освобождается память std::string после использования std::bind
Всем привет! Есть система, которая подгружает из внешних библиотек функции, упаковывает их в std::bind и заносит в std::map&lt;std::string,...

std::weak_ptr & std::enable_shared_for_this. Как передаем this?
#include &lt;iostream&gt; #include &lt;memory&gt; class SharedObject : public std::enable_shared_from_this&lt;SharedObject&gt; { public: int x = 1; ...


Практическое применение



Понимание базового синтаксиса и наиболее распространенных способов применения поможет вам эффективно интегрировать std::span в свои проекты.

Базовый синтаксис и использование



Создать std::span можно различными способами в зависимости от источника данных. Рассмотрим наиболее типичные варианты:

C++
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
#include <span>
#include <vector>
#include <array>
#include <iostream>
 
int main() {
    // Создание span из C-массива
    int rawArray[] = {1, 2, 3, 4, 5};
    std::span<int> spanFromRawArray(rawArray);
    
    // Создание span из std::vector
    std::vector<int> vec = {10, 20, 30, 40, 50};
    std::span<int> spanFromVector(vec);
    
    // Создание span из std::array
    std::array<int, 3> arr = {100, 200, 300};
    std::span<int> spanFromArray(arr);
    
    // Доступ к элементам через span
    spanFromVector[0] = 15; // Изменение первого элемента вектора через span
    
    // Вывод элементов исходного вектора показывает, что изменения отражаются
    for (int num : vec) {
        std::cout << num << " ";
    }
    // Выведет: 15 20 30 40 50
    
    return 0;
}
В этом примере мы создаем std::span из различных типов контейнеров. Обратите внимание, что изменения, сделанные через span, отражаются в исходных данных, поскольку span просто предоставляет доступ к ним, а не копирует их.
С C++17 также можно использовать автоматический вывод типов с помощью Class Template Argument Deduction (CTAD), что делает код ещё компактнее:

C++
1
2
int rawArray[] = {1, 2, 3, 4, 5};
std::span spanFromRawArray(rawArray); // Компилятор выведет std::span<int>

Работа с фиксированным и динамическим размером



Как уже упоминалось, std::span может иметь фиксированный или динамический размер. Вот как они используются на практике:

C++
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
#include <span>
#include <vector>
#include <iostream>
 
void processFixedSpan(std::span<int, 3> fixed) {
    std::cout << "Обработка span с фиксированным размером: ";
    for (int value : fixed) {
        std::cout << value << " ";
    }
    std::cout << std::endl;
}
 
void processDynamicSpan(std::span<int> dynamic) {
    std::cout << "Обработка span с динамическим размером: ";
    for (int value : dynamic) {
        std::cout << value << " ";
    }
    std::cout << std::endl;
}
 
int main() {
    int array[3] = {1, 2, 3};
    std::vector<int> vec = {4, 5, 6, 7};
    
    // Span с фиксированным размером
    std::span<int, 3> fixedSpan(array);
    processFixedSpan(fixedSpan);
    
    // Span с динамическим размером
    std::span<int> dynamicSpan(vec);
    processDynamicSpan(dynamicSpan);
    
    // Этот код вызовет ошибку компиляции, так как размер не совпадает
    // processFixedSpan(dynamicSpan);
    
    return 0;
}
Span с фиксированным размером обеспечивает дополнительную безопасность типов на уровне компиляции. Если вы попытаетесь создать span с фиксированным размером из контейнера с неправильным количеством элементов, компилятор выдаст ошибку. Это может быть полезно в ситуациях, когда вы точно знаете, с каким размером данных должна работать ваша функция.

Работа с частями данных через subspan



Одна из мощных возможностей std::span — это создание представлений для частей данных без их копирования. Метод subspan позволяет выделить подмножество элементов из существующего span:

C++
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
#include <span>
#include <vector>
#include <iostream>
 
void printSpan(std::span<const int> data, const std::string& label) {
    std::cout << label << ": ";
    for (int value : data) {
        std::cout << value << " ";
    }
    std::cout << std::endl;
}
 
int main() {
    std::vector<int> numbers = {10, 20, 30, 40, 50, 60, 70, 80};
    std::span<int> fullSpan(numbers);
    
    // Создание subspan, начиная с индекса 2, длиной 4 элемента
    auto middleSpan = fullSpan.subspan(2, 4);
    printSpan(middleSpan, "Средняя часть");  // Выведет: 30 40 50 60
    
    // Создание subspan, начиная с индекса 6 до конца
    auto endSpan = fullSpan.subspan(6);
    printSpan(endSpan, "Конец");  // Выведет: 70 80
    
    // Изменение элемента через subspan отражается в оригинальных данных
    middleSpan[1] = 45;
    printSpan(fullSpan, "После изменения");  // В элементе с индексом 3 будет 45
    
    return 0;
}
Метод subspan принимает два параметра: начальный индекс и количество элементов. Если второй параметр не указан, span будет включать все элементы от начального индекса до конца исходного span.

Примеры использования в реальном коде



Давайте рассмотрим несколько практических примеров, где std::span может существенно улучшить код.

Пример 1: Обработка сигналов или данных датчиков



C++
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#include <span>
#include <vector>
#include <algorithm>
#include <numeric>
 
// Функция для нахождения среднего значения в окне данных
double computeMovingAverage(std::span<const double> data, size_t windowSize) {
    if (data.size() < windowSize || windowSize == 0) {
        return 0.0;
    }
    
    double sum = std::accumulate(data.begin(), data.begin() + windowSize, 0.0);
    return sum / windowSize;
}
 
// Функция для обработки всех данных с использованием скользящего окна
std::vector<double> processWithMovingAverage(std::span<const double> signal, size_t windowSize) {
    std::vector<double> result;
    result.reserve(signal.size() - windowSize + 1);
    
    for (size_t i = 0; i <= signal.size() - windowSize; ++i) {
        double average = computeMovingAverage(signal.subspan(i, windowSize), windowSize);
        result.push_back(average);
    }
    
    return result;
}
В этом примере std::span используется для создания "окон" данных без копирования, что делает обработку сигналов более эффективной.

Пример 2: Работа с матрицами



C++
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
#include <span>
#include <vector>
#include <iostream>
 
// Функция для суммирования элементов в строке матрицы
int sumRow(std::span<const int> row) {
    int result = 0;
    for (int value : row) {
        result += value;
    }
    return result;
}
 
// Функция для работы с двумерной матрицей
void processMatrix(std::span<const int> matrix, int rows, int cols) {
    for (int i = 0; i < rows; ++i) {
        // Выделяем одну строку из линеаризованной матрицы
        auto row = matrix.subspan(i * cols, cols);
        std::cout << "Сумма элементов строки " << i << ": " << sumRow(row) << std::endl;
    }
}
 
int main() {
    // Создаем линеаризованную матрицу 3x3
    std::vector<int> matrix = {
        1, 2, 3,  // Строка 0
        4, 5, 6,  // Строка 1
        7, 8, 9   // Строка 2
    };
    
    processMatrix(matrix, 3, 3);
    
    return 0;
}
Этот пример демонстрирует, как std::span может использоваться для работы с многомерными данными, представленными в линеаризованном виде.

Пример 3: Интеграция с существующим кодом



Допустим, у вас есть C-API, которое требует указателей и размеров для работы с массивами данных:

C++
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// Старый C-API
extern "C" {
    void process_data(const float* data, size_t count);
    int analyze_buffer(int* buffer, size_t size);
}
 
// Современный C++ код, использующий span
#include <span>
#include <vector>
 
void processSensorData(std::span<const float> sensorData) {
    // Вызов C-API с данными из span
    process_data(sensorData.data(), sensorData.size());
}
 
int analyzeAndUpdateValues(std::span<int> values) {
    // Вызов C-API, который может модифицировать данные
    return analyze_buffer(values.data(), values.size());
}
Этот пример показывает, как std::span может служить мостом между современным C++ кодом и более старым C-API, предоставляя удобный и безопасный интерфейс со стороны C++.

Техники преобразования других контейнеров в std::span



Создание std::span из стандартных контейнеров обычно прямолинейно, но есть несколько нюансов и дополнительных техник, которые полезно знать:

C++
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
#include <span>
#include <vector>
#include <array>
#include <string>
 
void demonstrateSpanConversions() {
    // Создание из std::array
    std::array<double, 5> arr = {1.1, 2.2, 3.3, 4.4, 5.5};
    std::span<double, 5> spanFromArray(arr);  // Фиксированный размер
    std::span<double> dynamicSpanFromArray(arr);  // Динамический размер
    
    // Создание из std::vector
    std::vector<int> vec = {10, 20, 30};
    std::span<int> spanFromVector(vec);
    
    // Создание из std::string (как последовательности символов)
    std::string str = "Hello";
    std::span<const char> spanFromString(str);
    
    // Создание из подмножества std::vector
    if (vec.size() >= 2) {
        std::span<int> partialSpan(vec.data() + 1, 2);  // Элементы 20, 30
    }
    
    // Создание span для const-данных из неконстантных
    std::span<const int> constSpanFromVector(vec);
    
    // ПРИМЕЧАНИЕ: Следующий код не скомпилируется из-за нарушения const-корректности
    // std::vector<const int> constVec = {1, 2, 3}; // Вектор из const-элементов не допускается
    // std::span<int> nonConstSpanFromConst(constSpanFromVector); // Нарушение const-корректности
}
Обратите внимание на использование конструктора, принимающего указатель и размер — это полезно, когда вам нужно создать span для части контейнера, не используя метод subspan.

Специальный случай: Работа с C-массивами



При работе с C-массивами есть дополнительные нюансы, о которых стоит помнить:

C++
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#include <span>
#include <iostream>
 
void processDoubleArray(std::span<double> data) {
    // Обработка массива double
}
 
int main() {
    // C-массив на стеке
    double stackArray[5] = {1.0, 2.0, 3.0, 4.0, 5.0};
    processDoubleArray(stackArray);  // OK
    
    // Динамически выделенный массив
    double* heapArray = new double[5]{6.0, 7.0, 8.0, 9.0, 10.0};
    
    // ВНИМАНИЕ: Следующий код требует явного указания размера,
    // так как для динамического массива компилятор не знает его размер
    // processDoubleArray(heapArray);  // Ошибка! Размер неизвестен
    
    // Правильный способ с явным указанием размера
    processDoubleArray(std::span<double>(heapArray, 5));
    
    delete[] heapArray;
    
    return 0;
}
Этот пример показывает важное различие между массивами на стеке, размер которых известен на этапе компиляции, и динамически выделенными массивами, для которых размер необходимо указывать явно при создании std::span.

Работа с const-данными



Правильное использование std::span<const T> для доступа только для чтения — важный аспект const-корректного программирования:

C++
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
#include <span>
#include <vector>
#include <iostream>
 
// Функция, которая только читает данные
void printData(std::span<const int> data) {
    for (const int value : data) {
        std::cout << value << " ";
    }
    std::cout << std::endl;
}
 
// Функция, которая модифицирует данные
void doubleValues(std::span<int> data) {
    for (int& value : data) {
        value *= 2;
    }
}
 
int main() {
    std::vector<int> numbers = {1, 2, 3, 4, 5};
    
    // Используем const span для функции, которая только читает данные
    printData(numbers);
    
    // Используем неконстантный span для функции, которая модифицирует данные
    doubleValues(numbers);
    
    // Проверяем, что данные изменились
    printData(numbers);
    
    // Для константных контейнеров мы можем использовать только const span
    const std::vector<int> constNumbers = {6, 7, 8};
    printData(constNumbers);
    
    // Следующий код вызвал бы ошибку компиляции
    // doubleValues(constNumbers);
    
    return 0;
}
Использование std::span<const T> для функций, которые не модифицируют данные, делает код более понятным и безопасным, предотвращая случайное изменение данных.

Производительность и оптимизация



Одним из ключевых преимуществ std::span является его эффективность при работе с последовательностями данных. По сравнению с альтернативными подходами, std::span обеспечивает минимальные накладные расходы, что делает его привлекательным выбором для производительно-критичных приложений. Но насколько на самом деле эффективен std::span и в каких ситуациях его использование оправдано с точки зрения производительности?

Сравнение с альтернативными подходами



Для оценки эффективности std::span сравним его с другими распространенными способами передачи данных в функции:

1. Передача по значению (копирование):
C++
1
2
3
4
5
6
   void processVector(std::vector<int> vec) {
       // Полное копирование вектора
       for (auto& elem : vec) {
           elem *= 2;
       }
   }
2. Передача по ссылке:
C++
1
2
3
4
5
6
   void processVectorRef(std::vector<int>& vec) {
       // Без копирования, но ограничено типом std::vector
       for (auto& elem : vec) {
           elem *= 2;
       }
   }
3. Передача указателя и размера:
C++
1
2
3
4
5
6
   void processArray(int* data, size_t size) {
       // Без копирования, но без безопасности типов
       for (size_t i = 0; i < size; ++i) {
           data[i] *= 2;
       }
   }
4. Использование std::span:
C++
1
2
3
4
5
6
   void processSpan(std::span<int> data) {
       // Без копирования, с безопасностью типов и универсальностью
       for (auto& elem : data) {
           elem *= 2;
       }
   }
С точки зрения накладных расходов на память и время выполнения, std::span значительно превосходит передачу по значению, поскольку не требует копирования данных. Внутренняя реализация std::span обычно состоит всего из двух полей: указателя на данные и количества элементов (или указателя на последний элемент), что делает его чрезвычайно легковесным объектом. Что касается сравнения с передачей по ссылке, std::span имеет преимущество в универсальности: одна и та же функция может принимать различные типы контейнеров без необходимости в перегрузках. Это не только упрощает код, но и позволяет избежать дублирования реализаций.

По сравнению с традиционным подходом "указатель + размер", std::span предлагает сопоставимую производительность, но с дополнительными преимуществами безопасности и удобства. Компиляторы современных C++ обычно оптимизируют код со std::span до уровня, эквивалентного прямой работе с указателями.
Интересные результаты можно наблюдать при тестировании производительности:

C++
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
#include <span>
#include <vector>
#include <chrono>
#include <iostream>
 
// Функции для тестирования различных подходов
void sumWithVector(const std::vector<int>& vec) {
    volatile int sum = 0;
    for (int value : vec) {
        sum += value;
    }
}
 
void sumWithPointer(const int* data, size_t size) {
    volatile int sum = 0;
    for (size_t i = 0; i < size; ++i) {
        sum += data[i];
    }
}
 
void sumWithSpan(std::span<const int> data) {
    volatile int sum = 0;
    for (int value : data) {
        sum += value;
    }
}
 
int main() {
    const int iterations = 1000000;
    std::vector<int> testData(1000, 1);
    
    // Тест с std::vector
    auto start = std::chrono::high_resolution_clock::now();
    for (int i = 0; i < iterations; ++i) {
        sumWithVector(testData);
    }
    auto end = std::chrono::high_resolution_clock::now();
    std::cout << "Vector: " << std::chrono::duration_cast<std::chrono::milliseconds>(end - start).count() << " ms\n";
    
    // Тест с указателем
    start = std::chrono::high_resolution_clock::now();
    for (int i = 0; i < iterations; ++i) {
        sumWithPointer(testData.data(), testData.size());
    }
    end = std::chrono::high_resolution_clock::now();
    std::cout << "Pointer: " << std::chrono::duration_cast<std::chrono::milliseconds>(end - start).count() << " ms\n";
    
    // Тест со std::span
    start = std::chrono::high_resolution_clock::now();
    for (int i = 0; i < iterations; ++i) {
        sumWithSpan(testData);
    }
    end = std::chrono::high_resolution_clock::now();
    std::cout << "Span: " << std::chrono::duration_cast<std::chrono::milliseconds>(end - start).count() << " ms\n";
    
    return 0;
}
В большинстве случаев результаты показывают, что std::span имеет производительность, почти идентичную прямому использованию указателей, и значительно превосходит передачу по значению. Это особенно заметно при работе с большими объемами данных или при частых вызовах функций.

Подводные камни и ограничения



Несмотря на все свои преимущества, std::span имеет ряд ограничений и особенностей, о которых нужно помнить для оптимального использования:

1. Контроль времени жизни данных: std::span не увеличивает время жизни объектов, на которые он ссылается. Использование std::span, указывающего на уничтоженные данные, приведет к неопределенному поведению:

C++
1
2
3
4
5
6
7
8
9
   std::span<int> createBadSpan() {
       std::vector<int> tempData = {1, 2, 3};
       return std::span<int>(tempData);  // tempData будет уничтожен!
   }
   
   void usageExample() {
       auto span = createBadSpan();  // span теперь указывает на недействительную память
       int value = span[0];  // Неопределенное поведение!
   }
2. Накладные расходы для фиксированного размера: Хотя std::span с динамическим размером (std::span<T>) имеет минимальные накладные расходы, версия с фиксированным размером (std::span<T, N>) может потреблять больше памяти из-за необходимости хранения размера как шаблонного параметра.

3. Ограничение только на непрерывные последовательности: std::span может работать только с контейнерами, элементы которых хранятся в непрерывной памяти. Это исключает использование со структурами данных вроде std::list или ассоциативных контейнеров.

4. Возможное снижение производительности при частом создании subspan: Хотя сам std::span очень эффективен, частое создание новых span через метод subspan может привести к накладным расходам. В критичном к производительности коде может быть лучше повторно использовать существующие span вместо создания новых.

5. Переполнение размера: При работе с очень большими массивами данных возможно переполнение size_t, что может привести к неожиданному поведению. Это редкий сценарий, но о нем стоит помнить при работе с экстремально большими объемами данных.

Профилирование кода с использованием std::span



Для оптимального использования std::span в производительно-критичных приложениях рекомендуется проводить профилирование. Вот несколько советов:

1. Измерение накладных расходов на создание span: Хотя создание std::span обычно имеет минимальные накладные расходы, в некоторых сценариях (особенно внутри горячих циклов) даже эти расходы могут быть значимыми. Профилирование поможет выявить такие случаи.

C++
1
2
3
4
5
6
7
8
9
10
11
   // Потенциальная проблема - создание span в цикле
   for (int i = 0; i < 1000000; ++i) {
       std::span<int> tempSpan(largeVector);
       // Обработка данных через tempSpan
   }
   
   // Лучший вариант - создание span за пределами цикла
   std::span<int> reusableSpan(largeVector);
   for (int i = 0; i < 1000000; ++i) {
       // Обработка данных через reusableSpan
   }
2. Сравнение производительности различных подходов: В некоторых случаях передача по ссылке может быть эффективнее использования std::span, особенно если вы работаете только с одним типом контейнера. Профилирование поможет принять обоснованное решение.

3. Оценка влияния bounds checking: В режиме отладки некоторые реализации std::span могут включать дополнительные проверки границ, что может замедлить выполнение. Важно проверять производительность в релизных сборках, где эти проверки обычно отключены.

Случаи неэффективности применения std::span



Хотя std::span весьма эффективен в большинстве случаев, существуют сценарии, когда его использование может быть не оптимальным:
1. Очень короткие последовательности с известным размером: Для очень маленьких массивов с фиксированным размером (например, координаты точки в 2D или 3D пространстве) использование std::span может быть излишним. В таких случаях прямая передача элементов или использование структур может быть эффективнее.
2. Случаи, когда владение данными критично: Если семантика владения важна для логики программы, использование умных указателей или контейнеров, явно выражающих владение, может быть предпочтительнее.
3. Работа с несмежными данными: Для алгоритмов, которым нужен доступ к несмежным элементам или которые требуют особых паттернов доступа (например, через итераторы с произвольным доступом), прямое использование итераторов может быть более подходящим.
4. Функции с множеством перегрузок для разных типов: Если функция уже имеет множество специализаций для разных типов контейнеров с оптимизированной для каждого типа реализацией, использование std::span может не дать значимых преимуществ.

Примером неэффективного использования std::span может быть:

C++
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// Неэффективно: создание временного вектора и затем span
std::vector<int> createAndProcess() {
    std::vector<int> result = {1, 2, 3};
    std::span<int> resultSpan(result);  // Излишне
    processData(resultSpan);  // Можно просто передать result
    return result;
}
 
// Неэффективно: частое создание subspan во вложенных циклах
void processByBlocks(std::span<int> data, int blockSize) {
    for (size_t i = 0; i < data.size(); i += blockSize) {
        for (size_t j = 0; j < blockSize && i + j < data.size(); ++j) {
            auto element = data.subspan(i + j, 1);  // Создание subspan для одного элемента избыточно
            processElement(element[0]);  // Лучше использовать data[i + j]
        }
    }
}
Подводя итог, std::span является мощным инструментом, который в большинстве случаев обеспечивает отличную производительность при работе с последовательностями данных. Однако, как и любой инструмент, он должен применяться с пониманием его сильных сторон и ограничений. Профилирование и измерение производительности в конкретных сценариях помогут определить, когда использование std::span принесет наибольшую выгоду.

Расширенные возможности



std::span предлагает не только базовый функционал для работы с непрерывными последовательностями данных, но и ряд расширенных возможностей, которые делают его еще более мощным инструментом в арсенале C++ разработчика. Рассмотрим подробнее, как std::span взаимодействует с алгоритмами STL, какие нестандартные приёмы можно использовать с его помощью, и как он применяется в многопоточном программировании.

Интеграция с алгоритмами STL



Одно из главных преимуществ std::span — идеальная совместимость со стандартными алгоритмами из STL. Поскольку std::span предоставляет итераторы, соответствующие требованиям STL, его можно напрямую использовать со всеми алгоритмами стандартной библиотеки:

C++
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
#include <span>
#include <vector>
#include <algorithm>
#include <numeric>
#include <iostream>
 
int main() {
    std::vector<int> numbers = {5, 2, 8, 1, 9, 3, 7, 4, 6};
    std::span<int> numbersSpan(numbers);
    
    // Сортировка через std::span
    std::sort(numbersSpan.begin(), numbersSpan.end());
    
    // Проверка, что все элементы отсортированы
    bool isSorted = std::is_sorted(numbersSpan.begin(), numbersSpan.end());
    std::cout << "Отсортировано: " << (isSorted ? "Да" : "Нет") << std::endl;
    
    // Подсчёт суммы элементов
    int sum = std::accumulate(numbersSpan.begin(), numbersSpan.end(), 0);
    std::cout << "Сумма: " << sum << std::endl;
    
    // Преобразование всех элементов (умножение на 2)
    std::transform(numbersSpan.begin(), numbersSpan.end(), numbersSpan.begin(),
                   [](int val) { return val * 2; });
    
    // Вывод преобразованных элементов
    for (int val : numbersSpan) {
        std::cout << val << " ";
    }
    std::cout << std::endl;
    
    return 0;
}
Особенно полезно сочетание std::span с алгоритмами, которые работают с частями последовательностей. Например, можно эффективно реализовать скользящее окно или разделить последовательность на непересекающиеся блоки:

C++
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
#include <span>
#include <vector>
#include <iostream>
#include <algorithm>
 
// Функция для обработки блоков данных
void processDataBlocks(std::span<const int> data, size_t blockSize) {
    for (size_t offset = 0; offset < data.size(); offset += blockSize) {
        // Вычисляем размер текущего блока (может быть меньше blockSize в конце)
        size_t currentBlockSize = std::min(blockSize, data.size() - offset);
        
        // Создаём span для текущего блока
        auto block = data.subspan(offset, currentBlockSize);
        
        // Обрабатываем блок (например, находим максимальный элемент)
        auto maxElement = std::max_element(block.begin(), block.end());
        
        std::cout << "Блок начиная с индекса " << offset 
                  << ", максимальный элемент: " << *maxElement << std::endl;
    }
}
 
int main() {
    std::vector<int> data = {3, 1, 4, 1, 5, 9, 2, 6, 5, 3, 5};
    
    // Обработка данных блоками по 3 элемента
    processDataBlocks(data, 3);
    
    return 0;
}

Нестандартные приёмы



С помощью std::span можно реализовать ряд интересных и нетривиальных техник, которые упрощают работу с данными и делают код более выразительным.

Реализация матричных операций



Хотя std::span одномерен по своей природе, с его помощью можно удобно работать с многомерными данными, представленными в линейной памяти:

C++
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
#include <span>
#include <vector>
#include <iostream>
 
// Функция для работы с матрицей через std::span
void printMatrix(std::span<const int> matrix, size_t rows, size_t cols) {
    for (size_t r = 0; r < rows; ++r) {
        for (size_t c = 0; c < cols; ++c) {
            std::cout << matrix[r * cols + c] << "\t";
        }
        std::cout << std::endl;
    }
}
 
// Функция для транспонирования матрицы
std::vector<int> transposeMatrix(std::span<const int> matrix, size_t rows, size_t cols) {
    std::vector<int> result(rows * cols);
    
    for (size_t r = 0; r < rows; ++r) {
        for (size_t c = 0; c < cols; ++c) {
            result[c * rows + r] = matrix[r * cols + c];
        }
    }
    
    return result;
}
 
int main() {
    // Создаём матрицу 3x3 в линейном представлении
    std::vector<int> matrixData = {
        1, 2, 3,
        4, 5, 6,
        7, 8, 9
    };
    
    std::cout << "Исходная матрица:" << std::endl;
    printMatrix(matrixData, 3, 3);
    
    auto transposed = transposeMatrix(matrixData, 3, 3);
    
    std::cout << "Транспонированная матрица:" << std::endl;
    printMatrix(transposed, 3, 3);
    
    return 0;
}

Реализация циклической очереди



С помощью std::span можно реализовать эффективную циклическую очередь без копирования данных:

C++
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
#include <span>
#include <vector>
#include <iostream>
 
class CircularBuffer {
private:
    std::vector<int> data;
    size_t head = 0;
    size_t size = 0;
    
public:
    explicit CircularBuffer(size_t capacity) : data(capacity) {}
    
    void push(int value) {
        size_t insertPos = (head + size) % data.size();
        data[insertPos] = value;
        
        if (size < data.size()) {
            ++size;
        } else {
            // Буфер полный, смещаем head
            head = (head + 1) % data.size();
        }
    }
    
    // Получаем span текущего содержимого буфера
    std::span<const int> getElements() const {
        if (size == 0) {
            return {};
        }
        
        if (head + size <= data.size()) {
            // Данные находятся в непрерывном блоке
            return std::span<const int>(data.data() + head, size);
        } else {
            // Данные "обернуты" вокруг конца вектора
            // В этом случае span не может охватить все элементы сразу
            // Возвращаем только часть от head до конца вектора
            return std::span<const int>(data.data() + head, data.size() - head);
        }
    }
    
    // Получаем span для второй части (если данные "обернуты")
    std::span<const int> getWrappedElements() const {
        if (size == 0 || head + size <= data.size()) {
            return {};
        }
        
        // Вычисляем, сколько элементов находится в начале вектора
        size_t wrappedCount = (head + size) % data.size();
        return std::span<const int>(data.data(), wrappedCount);
    }
};

Работа с подмножествами и срезами данных через std::span



Одна из наиболее мощных возможностей std::span — создание представлений для подмножеств данных без их копирования. Метод subspan позволяет создавать "окна" в данные, что особенно полезно при работе с алгоритмами, которые оперируют подпоследовательностями.

C++
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
#include <span>
#include <vector>
#include <iostream>
#include <algorithm>
 
// Функция для поиска подпоследовательности с максимальной суммой
std::span<const int> findMaxSumSubarray(std::span<const int> data) {
    if (data.empty()) {
        return {};
    }
    
    int maxSum = data[0];
    int currentSum = data[0];
    
    size_t maxStart = 0;
    size_t maxEnd = 1;
    size_t currentStart = 0;
    
    for (size_t i = 1; i < data.size(); ++i) {
        if (data[i] > currentSum + data[i]) {
            currentStart = i;
            currentSum = data[i];
        } else {
            currentSum += data[i];
        }
        
        if (currentSum > maxSum) {
            maxSum = currentSum;
            maxStart = currentStart;
            maxEnd = i + 1;
        }
    }
    
    return data.subspan(maxStart, maxEnd - maxStart);
}
 
int main() {
    std::vector<int> data = {-2, 1, -3, 4, -1, 2, 1, -5, 4};
    
    auto maxSubarray = findMaxSumSubarray(data);
    
    std::cout << "Подпоследовательность с максимальной суммой: ";
    for (int val : maxSubarray) {
        std::cout << val << " ";
    }
    std::cout << std::endl;
    
    int sum = std::accumulate(maxSubarray.begin(), maxSubarray.end(), 0);
    std::cout << "Сумма: " << sum << std::endl;
    
    return 0;
}

Использование std::span в многопоточном программировании



std::span может быть эффективно использован в многопоточных приложениях для разделения работы между потоками. Поскольку std::span предоставляет невладеющий доступ к данным, его можно безопасно передавать между потоками, если базовые данные не модифицируются или доступ к ним должным образом синхронизирован.

C++
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
#include <span>
#include <vector>
#include <thread>
#include <numeric>
#include <iostream>
#include <mutex>
 
std::mutex outputMutex;
 
// Функция для обработки части данных в отдельном потоке
void processDataChunk(std::span<const int> chunk, int threadId, std::vector<int>& results) {
    // Суммируем элементы в данном chunk
    int sum = std::accumulate(chunk.begin(), chunk.end(), 0);
    
    {
        std::lock_guard<std::mutex> lock(outputMutex);
        std::cout << "Поток " << threadId << " обработал " << chunk.size() 
                  << " элементов, сумма: " << sum << std::endl;
    }
    
    results[threadId] = sum;
}
 
int main() {
    const int dataSize = 1000000;
    const int numThreads = 4;
    
    // Создаём большой массив данных
    std::vector<int> data(dataSize, 1);  // Все элементы равны 1 для простоты
    
    // Вектор для хранения результатов от каждого потока
    std::vector<int> results(numThreads, 0);
    
    // Вектор потоков
    std::vector<std::thread> threads;
    
    // Размер chunk для каждого потока
    size_t chunkSize = dataSize / numThreads;
    
    // Создаём и запускаем потоки
    for (int i = 0; i < numThreads; ++i) {
        size_t start = i * chunkSize;
        size_t end = (i == numThreads - 1) ? dataSize : start + chunkSize;
        
        threads.emplace_back(processDataChunk, 
                            std::span<const int>(&data[start], end - start), 
                            i, std::ref(results));
    }
    
    // Ждём завершения всех потоков
    for (auto& thread : threads) {
        thread.join();
    }
    
    // Суммируем результаты от всех потоков
    int totalSum = std::accumulate(results.begin(), results.end(), 0);
    std::cout << "Общая сумма: " << totalSum << std::endl;
    
    return 0;
}
Этот пример демонстрирует, как std::span может использоваться для эффективного разделения работы между потоками без необходимости копирования данных. Каждый поток получает свой "кусок" исходных данных через std::span, что минимизирует накладные расходы на передачу данных.

Расширенные возможности std::span делают его незаменимым инструментом для современного C++ программирования, позволяя писать более чистый, безопасный и эффективный код при работе с последовательностями данных.

Почему некоторые пишут std::, когда гораздо удобнее один раз написать using namespace std?
Почему некоторые пишут std::, когда гораздо удобнее писать using namespace std; один раз на весь код?

std::string, std::fstream, ошибка кучи
где то начало вылетать при операции += с локальной переменной std::string. Заменил на свой qString. Замечательно, то же самое... ошибка при _data =...

std::optional<T> при std::is_destructible_v<T> == false
Всем привет! Исследую несколько разных реализаций std::optional, и наткнулся на интересную вещь: реализация gcc допускает класть в optional типы,...

Signal & Slot введение
Добрый вечер. Помогите пожалуйста разобраться, если розбираетесь в Signal &amp; Slot. Уже не 1 неделю не могу уловить сути... LVL 1 Для начала...

Корректное введение номера телефона с маской
Задавал прошлый вопрос, касаемо маски и того, как поставить курсор на конец набранного текста. Попытался вникнуть в логику, посмотрел примеры на...

Как проверить введение числа с плавающей запятой
Здравствуйте, изучаю Windows Forms, подскажите, пожалуйста, как можно проверить, что было введено число с запятой? Если для int можно использовать...

Как проинициализировать std::stack<const int> obj ( std::stack<int>{} );
добрый день. вопрос в коде: http://rextester.com/VCVVML6656 #include &lt;iostream&gt; #include &lt;stack&gt; //-std=c++14 -fopenmp -O2 -g3...

std::filesystem && std::asio и пр
Пытался найти хоть какие-то сроки включения всего этого в стандарт (так же ожидается lexical_cast, any, string_algo и т.д.) и вообщем везде написано...

QTableView::setSpan: single cell span won't be added
Строю таблицу по координатам используя QTableWidget tblw-&gt;setSpan(koordinata_y, koordinata_x, koordinata_height, koordinata_width); Всё...

Представление памяти std::vector
Добрый день! Вопрос по вектору, из его описание следует, что он располагает свои элементы линейно до тех пор пока это возможно, а в случае если это...

<span align="left"> Заявитель: Фамилия </span> <span align="right">_____</span><br>
Какие теги использовать и как чтоб фраза Заявитель: Фамилия И.О. была слева на странице, а в той же строке, но прижатой к правой стороне было...

Скрыть span при нажатии вне этого span js
Подскажите пожалуйста, есть код: $(document).ready( function() { $('#valuta').keyup(function(){ ...

Метки c++, c++20, std::span
Размещено в Без категории
Надоела реклама? Зарегистрируйтесь и она исчезнет полностью.
Всего комментариев 0
Комментарии
 
Новые блоги и статьи
Согласованность транзакций в MongoDB
Codd 30.04.2025
MongoDB, начинавшая свой путь как классическая NoSQL система с акцентом на гибкость и масштабируемость, сильно спрогрессировала, включив в свой арсенал поддержку транзакционной согласованности. Это. . .
Продвинутый ввод-вывод в Java: NIO, NIO.2 и асинхронный I/O
Javaican 30.04.2025
Когда речь заходит о вводе-выводе в Java, классический пакет java. io долгие годы был единственным вариантом для разработчиков, но его ограничения становились всё очевиднее с ростом требований к. . .
Обнаружение объектов в реальном времени на Python с YOLO и OpenCV
AI_Generated 29.04.2025
Компьютерное зрение — одна из самых динамично развивающихся областей искусственного интеллекта. В нашем мире, где визуальная информация стала доминирующим способом коммуникации, способность машин. . .
Эффективные парсеры и токенизаторы строк на C#
UnmanagedCoder 29.04.2025
Обработка текстовых данных — частая задача в программировании, с которой сталкивается почти каждый разработчик. Парсеры и токенизаторы составляют основу множества современных приложений: от. . .
C++ в XXI веке - Эволюция языка и взгляд Бьярне Страуструпа
bytestream 29.04.2025
C++ существует уже более 45 лет с момента его первоначальной концепции. Как и было задумано, он эволюционировал, отвечая на новые вызовы, но многие разработчики продолжают использовать C++ так, будто. . .
Слабые указатели в Go: управление памятью и предотвращение утечек ресурсов
golander 29.04.2025
Управление памятью — один из краеугольных камней разработки высоконагруженных приложений. Го (Go) занимает уникальную нишу в этом вопросе, предоставляя разработчикам автоматическое управление памятью. . .
Разработка кастомных расширений для компилятора C++
NullReferenced 29.04.2025
Создание кастомных расширений для компиляторов C++ — инструмент оптимизации кода, внедрения новых языковых функций и автоматизации задач. Многие разработчики недооценивают гибкость современных. . .
Гайд по обработке исключений в C#
stackOverflow 29.04.2025
Разработка надёжного программного обеспечения невозможна без грамотной обработки исключительных ситуаций. Любая программа, независимо от её размера и сложности, может столкнуться с непредвиденными. . .
Создаем RESTful API с Laravel
Jason-Webb 28.04.2025
REST (Representational State Transfer) — это архитектурный стиль, который определяет набор принципов для создания веб-сервисов. Этот подход к построению API стал стандартом де-факто в современной. . .
Дженерики в C# - продвинутые техники
stackOverflow 28.04.2025
История дженериков началась с простой идеи — создать механизм для разработки типобезопасного кода без потери производительности. До их появления программисты использовали неуклюжие преобразования. . .
КиберФорум - форум программистов, компьютерный форум, программирование
Powered by vBulletin
Copyright ©2000 - 2025, CyberForum.ru