С появлением стандарта 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 <iostream>
#include <list>
#include <string>
#include <utility>
using lp = std::list<std::pair<std::string, int>>;
auto f(lp... Не могу разобраться как обновить в std::map<std::string, вектор_структур> Не могу разобраться как обновить вектор структур после его добавления в map без удаления и перезаписи
struct pStruct
{
int a;
... Не освобождается память std::string после использования std::bind Всем привет!
Есть система, которая подгружает из внешних библиотек функции, упаковывает их в std::bind и заносит в std::map<std::string,... std::weak_ptr & std::enable_shared_for_this. Как передаем this? #include <iostream>
#include <memory>
class SharedObject : public std::enable_shared_from_this<SharedObject>
{
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 & Slot.
Уже не 1 неделю не могу уловить сути...
LVL 1
Для начала... Корректное введение номера телефона с маской Задавал прошлый вопрос, касаемо маски и того, как поставить курсор на конец набранного текста. Попытался вникнуть в логику, посмотрел примеры на... Как проверить введение числа с плавающей запятой Здравствуйте, изучаю Windows Forms, подскажите, пожалуйста, как можно проверить, что было введено число с запятой? Если для int можно использовать... Как проинициализировать std::stack<const int> obj ( std::stack<int>{} ); добрый день.
вопрос в коде:
http://rextester.com/VCVVML6656
#include <iostream>
#include <stack>
//-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->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(){
...
|