Работа с последовательностями данных – одна из фундаментальных задач, с которой сталкивается каждый разработчик. C++ прошел длинный путь в эволюции средств для манипуляции коллекциями – от использования массивов в C-стиле до современной библиотеки ranges. Однако некоторые казалось бы простые операции, как объединение нескольких последовательностей, долгое время требовали от разработчиков написания избыточного кода. Представим, что нужно объединить несколько векторов, списков или других контейнеров в один логический поток данных. До появления новых возможностей в C++20/23/26 разработчики были вынуждены использовать несколько подходов, каждый со своими недостатками:
| C++ | 1
2
3
4
5
6
| // Традиционный подход: копирование в новый контейнер
std::vector<int> v1 = {1, 2, 3};
std::vector<int> v2 = {4, 5, 6};
std::vector<int> result;
result.insert(result.end(), v1.begin(), v1.end());
result.insert(result.end(), v2.begin(), v2.end()); |
|
Этот подход создает избыточные копии элементов, потребляет дополнительную память и требует явного управления вставкой. Для случаев, когда требуется лишь итерация по объединенной последовательности без создания промежуточного контейнера, подобный подход неоптимален. Альтернативным решением могло быть написание собственного итератора, объединяющего несколько диапазонов, но это сложная задача, требующая глубокого понимания типажей итераторов и стандартной библиотеки. Код получается громоздким и подверженным ошибкам.
Сообщество C++ давно запрашивало более элегантные решения для этой проблемы. С появлением концепции диапазонов (ranges) в C++20 открылись новые возможности для работы с последовательностями данных. Библиотека ranges представляет эволюционный шаг вперед по сравнению с традиционной STL, предлагая более декларативный подход к алгоритмам и возможность компоновки операций в цепочки без промежуточных переменных. Эволюция обработки коллекций в C++ можно представить следующим образом:
1. Ранний C++: Ручные циклы и указатели.
2. C++98/03: Появление STL с контейнерами, итераторами и алгоритмами.
3. C++11/14: Улучшение удобства использования (auto, улучшенные for-циклы).
4. C++17: Расширение алгоритмов (параллельные алгоритмы).
5. C++20: Введение концепций ranges.
6. C++23/26: Дальнейшее развитие ranges с операциями join, concat и другими.
Значительное влияние на развитие стандартной библиотеки диапазонов оказала сторонняя библиотека range-v3, разработанная Эриком Нибблером. Эта библиотека стала экспериментальной площадкой для многих концепций, которые в итоге вошли в стандарт C++. Range-v3 предложила ленивые вычисления и композицию операций, ставшие определяющими характеристиками стандартных ranges. В C++20 была введена первая версия библиотеки ranges, включающая базовый функционал для работы с диапазонами. Она представила новый способ мышления о последовательностях: вместо пар итераторов, ranges рассматривают диапазон как единую абстракцию. Это упростило написание кода и сделало его более читаемым. Однако C++20 не предоставил всех необходимых примитивов для работы с диапазонами. В частности, отсутствовали удобные способы объединения нескольких диапазонов в единую последовательность без создания промежуточных контейнеров. Эта функциональность начала появляться в C++23 и получила дальнейшее развитие в C++26. Проблема объединения последовательностей имеет несколько аспектов:
1. Конкатенация независимых диапазонов (vector1 + vector2 + ...).
2. Сплющивание (flattening) вложенных диапазонов (vector<vector<int>> → flat sequence).
3. Объединение с разделителями или специальными элементами между диапазонами.
Каждый из этих сценариев требует своего подхода, и стандартная библиотека C++ постепенно вводила соответствующие инструменты для их эффективного решения.
В последующих разделах мы подробно рассмотрим три ключевых компонента для работы с объединением последовательностей:
std::ranges::join_view (C++20) для сплющивания вложенных диапазонов.
std::ranges::join_with_view (C++23) для объединения с разделителями.
std::ranges::concat_view (C++26) для конкатенации независимых диапазонов.
Предпосылки появления join и concat операций
История развития средств для работы с последовательностями в 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
27
| // Вариант 1: Создание нового контейнера и копирование элементов
template<typename Container>
Container concatenate(const Container& c1, const Container& c2) {
Container result = c1;
result.insert(result.end(), c2.begin(), c2.end());
return result;
}
// Вариант 2: Использование алгоритмов копирования
std::vector<int> concatenate(const std::vector<int>& v1,
const std::vector<int>& v2) {
std::vector<int> result(v1.size() + v2.size());
std::copy(v1.begin(), v1.end(), result.begin());
std::copy(v2.begin(), v2.end(), result.begin() + v1.size());
return result;
}
// Вариант 3: Ручное объединение в цикле
void process_concatenated(const std::vector<int>& v1,
const std::vector<int>& v2) {
for (const auto& item : v1) {
process(item);
}
for (const auto& item : v2) {
process(item);
}
} |
|
Эти подходы страдают от нескольких проблем:
1. Избыточное копирование данных.
2. Выделение дополнительной памяти.
3. Отсутствие ленивых вычислений.
4. Сложность поддержки и повторное использование кода.
5. Ограниченная композиция с другими операциями.
Решения в других языках программирования предлагали гораздо более элегантные подходы к этой проблеме. Например, в Python операция конкатенации списков предельно проста и выразительна:
| Python | 1
| combined_list = list1 + list2 + list3 |
|
Для ленивого вычисления можно использовать генераторы:
| Python | 1
2
3
4
5
6
7
| def concat_iterables(*iterables):
for it in iterables:
yield from it
# Использование
for item in concat_iterables(list1, list2, list3):
process(item) |
|
В JavaScript также есть компактные решения:
| JavaScript | 1
2
3
4
5
| // Для массивов
const combined = [...array1, ...array2, ...array3];
// Или с помощью concat
const combined = array1.concat(array2, array3); |
|
Функциональные языки программирования, такие как Haskell, Scala и F#, оказали значительное влияние на дизайн современных библиотек C++. В этих языках операции над коллекциями часто реализуются как комбинаторы высшего порядка, которые можно компоновать для создания сложных преобразований данных.
В Haskell, например, функция concat естественно объединяет список списков в один плоский список:
| Haskell | 1
2
| -- Объединение списка списков
concat :: [[a]] -> [a] |
|
Библиотека range-v3, предшественница стандартных ranges, была явно вдохновлена этим функциональным подходом. Она ввела концепцию представлений (views) как ленивых преобразований последовательностей, которые можно компоновать с помощью операций конвейера. Одним из ключевых преимуществ функционального подхода является отложенное вычисление (lazy evaluation), позволяющее избежать создания промежуточных коллекций. Например, в Scala:
| Java | 1
2
3
4
| val result = list1 ++ list2 ++ list3
.filter(_ > 0)
.map(_ * 2)
.take(10) |
|
Этот код создает цепочку преобразований, которые вычисляются только при фактическом доступе к элементам результата.
Концепция монад, широко используемая в функциональном программировании, также повлияла на дизайн операций join в C++. Монада List в Haskell имеет операцию join, которая сплющивает вложенные структуры:
| Haskell | 1
| join :: Monad m => m (m a) -> m a |
|
Для списков это эквивалентно функции concat. Операция join_view в C++ ranges имеет сходную семантику, позволяя преобразовать range of ranges в плоский range.
Развитие C++ библиотек для работы с коллекциями шло параллельно с растущим признанием парадигмы функционального программирования в индустрии. Библиотеки Boost, а затем и стандартная библиотека, начали включать концепции, заимствованные из функционального мира:
1. Функторы высшего порядка (std::function, лямбда-выражения).
2. Композиция функций (C++20 ranges pipelines).
3. Ленивые вычисления (views в ranges).
4. Монадоподобные типы (std::optional, std::future).
Важным фактором, который подтолкнул развитие новых возможностей для работы с диапазонами, стала эволюция компиляторов C++. Улучшения в оптимизации, продвинутый вывод типов и поддержка constexpr сделали возможным реализацию сложных абстракций с производительностью, сравнимой с ручно написанным кодом. Конкретно для операций join и concat существовали дополнительные технические сложности:
1. Необходимость хранения и управления состоянием при итерации через несколько диапазонов.
2. Обработка краевых случаев (пустые диапазоны, разные типы элементов).
3. Поддержка различных категорий итераторов (однонаправленные, двунаправленные, с произвольным доступом).
4. Сохранение и передача ссылок на оригинальные элементы без создания копий.
Стандартная библиотека C++ имела строгие критерии для включения новых компонентов. Каждое новое дополнение должно было быть:
1. Общего назначения и широко применимым.
2. Эффективным с точки зрения времени выполнения и памяти.
3. Хорошо интегрированным с существующими компонентами.
4. Устойчивым к ошибкам пользователя.
Библиотеки range-v3 и boost::range служили испытательными полигонами где эти идеи могли быть протестированы и отточены перед включением в стандарт. Они продемонстрировали практическую ценность операций join и concat в реальных проектах, что помогло обосновать их добавление в стандартную библиотеку.
Накопленный опыт показал, что разработчикам необходимы различные типы операций объединения для разных сценариев:
- Конкатенция независимых диапазонов с сохранением их типов (concat).
- Сплющивание вложенных диапазонов в один плоский диапазон (join).
- Объединение с добавлением разделителей между элементами (join_with).
Эти операции должны были бесшовно интегрироваться с существующей экосистемой ranges, поддерживая ленивые вычисления и композицию через операции конвейера.
Зачем std::ranges::for_each() возвращает итератор на end()? cppreference.com :
std::ranges::for_each()
Return value
{ std::ranges::next( std::move(first),... std::ranges::transform Подскажите, ЧЯДНТ?
struct S{
std::string str1;
std::string str2;
};... Как определить тип возвращаемого представления в std::ranges Добрый день,
Возможно определить тип возвращаемого значения при использование std::ranges?
... Std::vector<std::pair<std::vector<int>::iterator, std::vector<int>::iterator> Вопрос по вектору.
Допустим есть вектор,
std::vector<int> vec;
на каком - то этапе заполнения я...
Детальный разбор std::ranges::join
Операция объединения или сплющивания вложенных последовательностей — одна из базовых в обработке данных. В C++20 была введена std::ranges::join_view, предоставляющая элегантное решение для сплющивания диапазона диапазонов в единую последовательность. В отличие от операции конкатенации, которая объединяет несколько отдельных диапазонов, join_view работает с единственным диапазоном, элементами которого являются другие диапазоны (например, vector<vector<int>>), и "расплющивает" эту структуру в плоскую последовательность элементов. Базовый синтаксис использования join_view следующий:
| C++ | 1
2
3
4
5
6
7
8
9
10
11
12
13
| #include <ranges>
#include <vector>
#include <iostream>
int main() {
std::vector<std::vector<int>> nested{{1, 2}, {3, 4, 5}, {6, 7}};
auto joined = std::views::join(nested);
for (int value : joined) {
std::cout << value << ' ';
}
// Вывод: 1 2 3 4 5 6 7
} |
|
Ключевой особенностью join_view является то, что он создает "представление" данных без фактического копирования элементов. Это особенно важно при работе с большими объемами данных или в сценариях, когда требуется минимизировать использование памяти.
join_view работает с диапазоном диапазонов, где внутренние диапазоны могут быть разных типов, но должны содержать элементы совместимого типа. Например, можно объединить vector<int> и list<int>, если они находятся внутри контейнера высшего уровня:
| C++ | 1
2
3
4
5
6
7
8
9
10
| std::vector<std::variant<std::vector<int>, std::list<int>>> mixed_containers;
mixed_containers.push_back(std::vector<int>{1, 2, 3});
mixed_containers.push_back(std::list<int>{4, 5, 6});
auto process_container = [](auto& container) {
return std::views::all(std::get<std::remove_reference_t<decltype(container)>>(container));
};
auto joined = std::views::transform(mixed_containers, process_container)
| std::views::join; |
|
Хотя этот пример более сложный, он демонстрирует гибкость и мощь join_view при работе с гетерогенными контейнерами.
Одним из распространенных случаев использования join_view является обработка строковых данных. Например, если у нас есть вектор строк, и мы хотим работать с отдельными символами:
| C++ | 1
2
3
4
5
6
7
8
| std::vector<std::string> words{"Hello", "World", "C++"};
auto characters = std::views::join(words);
// Подсчет частоты символов
std::map<char, int> frequency;
for (char c : characters) {
frequency[std::tolower(c)]++;
} |
|
Этот код легко подсчитывает частоту появления каждого символа во всех строках вектора, не требуя предварительного объединения строк в одну.
join_view также эффективно работает с более сложными типами данных. Рассмотрим пример обработки данных, представленных в виде дерева:
| C++ | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
| struct Node {
std::string value;
std::vector<Node> children;
};
// Функция для преобразования дерева в плоский список значений
auto flatten_tree(const Node& root) {
return std::ranges::views::single(root)
| std::views::transform([](const Node& node) {
return std::views::concat(
std::views::single(node.value),
node.children | std::views::join
| std::views::transform([](const Node& child) {
return flatten_tree(child);
})
);
})
| std::views::join;
} |
|
В этом примере мы рекурсивно преобразуем дерево в плоский список значений, используя комбинацию single_view, transform_view и join_view.
Важно отметить, что join_view не предоставляет произвольный доступ даже если все базовые диапазоны поддерживают его. Это ограничение связано с природой операции объединения: для определения позиции n-го элемента объединенного диапазона необходимо пройти через все предыдущие элементы. С точки зрения производительности, join_view обеспечивает оптимальную эффективность для многих сценариев использования:
1. Он не создает временных копий данных.
2. Работает лениво — обрабатывает элементы только по запросу.
3. Поддерживает непрерывность итерации, что важно для оптимизаций компилятора.
Однако у такого подхода есть и определенные ограничения:
1. Отсутствие прямого доступа к элементам через оператор [].
2. Некоторые алгоритмы могут быть менее эффективными без произвольного доступа.
3. Вложенные диапазоны должны быть стабильными (не изменяться) во время итерации.
Ещё одно важное свойство join_view — он корректно обрабатывает пустые вложенные диапазоны, просто пропуская их:
| C++ | 1
2
3
| std::vector<std::vector<int>> nested{{}, {1, 2}, {}, {3, 4, 5}, {}};
auto joined = std::views::join(nested);
// joined содержит 1, 2, 3, 4, 5 |
|
При работе с генераторами и ленивыми последовательностями join_view также показывает свою мощь. Он позволяет создавать потенциально бесконечные последовательности, которые вычисляются только при фактическом доступе к элементам:
| C++ | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| // Создание генератора последовательностей Фибоначчи
auto fibonacci_generator = [start=std::array{0, 1}]() mutable {
auto result = start[0];
start = {start[1], start[0] + start[1]};
return result;
};
// Создание диапазона из 10 последовательностей по 5 чисел Фибоначчи
auto sequences = std::views::iota(0, 10)
| std::views::transform([&](int) {
return std::views::generate_n(fibonacci_generator, 5);
});
// Объединение всех последовательностей в одну
auto all_fibonacci = std::views::join(sequences); |
|
Здесь мы используем комбинацию iota_view, transform_view, generate_n_view и join_view для создания последовательности из 50 чисел Фибоначчи без явного хранения всех промежуточных результатов. Следует заметить, что join_view идеально вписывается в парадигму функционального программирования, которая становится все более популярной в C++. Подход, основанный на композиции функций и ленивых вычислениях, позволяет писать более декларативный и читаемый код. В сравнении с традиционными императивными подходами, использование join_view может значительно сократить количество кода и уменьшить риск ошибок:
| C++ | 1
2
3
4
5
6
7
8
9
10
11
| // Традиционный подход
std::vector<int> flatten(const std::vector<std::vector<int>>& nested) {
std::vector<int> result;
for (const auto& inner : nested) {
result.insert(result.end(), inner.begin(), inner.end());
}
return result;
}
// Подход с использованием join_view
auto flattened = nested | std::views::join; |
|
Второй вариант не только короче и читабельнее, но и потенциально более эффективен, так как не требует промежуточного хранения и копирования элементов.
Производительность join_view заслуживает отдельного рассмотрения. Ключевое преимущество этого подхода – избежание промежуточных копий данных. При обработке больших наборов данных это может привести к значительной экономии памяти и времени исполнения. Внутренняя реализация join_view использует сложный итератор, который отслеживает текущую позицию как во внешнем, так и во внутреннем диапазоне. Когда достигается конец внутреннего диапазона, итератор автоматически переходит к следующему внутреннему диапазону. Эта логика скрыта от пользователя, предоставляя простой унифицированный интерфейс.
| C++ | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| // Пример реализации итератора для join_view (упрощенно)
template<typename OuterRange>
class join_iterator {
private:
using outer_iterator = std::ranges::iterator_t<OuterRange>;
using inner_range = std::ranges::range_reference_t<OuterRange>;
using inner_iterator = std::ranges::iterator_t<inner_range>;
outer_iterator outer_it_;
outer_iterator outer_end_;
std::optional<inner_iterator> inner_it_;
std::optional<inner_iterator> inner_end_;
// Методы для поддержания внутреннего состояния...
}; |
|
Важно понимать, что ленивость вычислений — это не просто теоретический трюк. Она имеет реальное практическое значение особенно при работе с потенциально бесконечными последовательностями или когда мы хотим обрабатывать только часть большого набора данных.
Рассмотрим более сложный пример, который демонстрирует мощь join_view в сочетании с другими адаптерами диапазонов:
| 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
| // Чтение строк из нескольких файлов и обработка всех строк как единой последовательности
std::vector<std::string> filenames = {"data1.txt", "data2.txt", "data3.txt"};
auto file_contents = filenames
| std::views::transform([](const std::string& filename) {
std::ifstream file(filename);
std::vector<std::string> lines;
std::string line;
while (std::getline(file, line)) {
lines.push_back(line);
}
return lines;
})
| std::views::join;
// Поиск всех строк, содержащих определенное слово
auto matching_lines = file_contents
| std::views::filter([](const std::string& line) {
return line.find("important") != std::string::npos;
});
// Вывод первых 10 найденных строк
auto first_10_matches = matching_lines
| std::views::take(10);
for (const auto& line : first_10_matches) {
std::cout << line << '\n';
} |
|
В этом примере мы комбинируем несколько операций:
1. Преобразование имен файлов в их содержимое (строки).
2. Объединение всех строк из всех файлов в единую последовательность.
3. Фильтрация строк по определенному критерию.
4. Извлечение только первых 10 результатов.
Благодаря ленивым вычислениям, мы не загружаем все файлы целиком в память и не обрабатываем все строки, если достаточно найти первые 10 совпадений.
Когда речь идет о больших наборах данных, преимущества join_view становятся еще более очевидными. Традиционный подход мог бы выглядеть так:
| 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
| std::vector<std::string> getAllLines(const std::vector<std::string>& filenames) {
std::vector<std::string> allLines;
for (const auto& filename : filenames) {
std::ifstream file(filename);
std::string line;
while (std::getline(file, line)) {
allLines.push_back(line);
}
}
return allLines;
}
std::vector<std::string> findMatches(const std::vector<std::string>& lines,
const std::string& keyword) {
std::vector<std::string> matches;
for (const auto& line : lines) {
if (line.find(keyword) != std::string::npos) {
matches.push_back(line);
}
}
return matches;
}
// Использование
auto allLines = getAllLines(filenames);
auto allMatches = findMatches(allLines, "important");
std::vector<std::string> first10;
for (size_t i = 0; i < std::min(size_t(10), allMatches.size()); ++i) {
first10.push_back(allMatches[i]);
} |
|
Это решение требует создания трех отдельных временных контейнеров и полного чтения всех файлов, даже если мы заинтересованы только в первых нескольких совпадениях.
join_view корректно обрабатывает пустые последовательности, но это поведение заслуживает более детального рассмотрения. Когда внутри вложенного диапазона есть пустые поддиапазоны, они просто пропускаются:
| C++ | 1
2
3
4
5
6
7
8
9
10
| std::vector<std::vector<int>> data{
{}, // Пустой поддиапазон
{1, 2, 3},
{}, // Еще один пустой поддиапазон
{4, 5},
{} // И еще один
};
auto joined = std::views::join(data);
// Результат: 1, 2, 3, 4, 5 |
|
Но что произойдет, если весь внешний диапазон пуст или содержит только пустые поддиапазоны?
| C++ | 1
2
3
4
5
6
7
| std::vector<std::vector<int>> emptyData{};
auto joinedEmpty1 = std::views::join(emptyData);
// Результат: пустая последовательность
std::vector<std::vector<int>> onlyEmptyRanges{{}, {}, {}};
auto joinedEmpty2 = std::views::join(onlyEmptyRanges);
// Результат: пустая последовательность |
|
В обоих случаях результатом будет пустая последовательность. Такое поведение интуитивно понятно и соответствует математической операции объединения пустых множеств. Важно отметить, что join_view может работать с диапазонами любой вложенности, что позволяет "сплющивать" многоуровневые структуры:
| 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
| // Трехуровневая вложенность
std::vector<std::vector<std::vector<int>>> deeplyNested{
{
{1, 2},
{3, 4}
},
{
{5, 6},
{7, 8}
}
};
// Сплющивание одного уровня за раз
auto partiallyFlattened = std::views::join(deeplyNested);
// Тип: диапазон из std::vector<int>
auto fullyFlattened = std::views::join(partiallyFlattened);
// Тип: диапазон из int
// Или в одной операции
auto fullyFlattened2 = deeplyNested
| std::views::join
| std::views::join;
// Результат: 1, 2, 3, 4, 5, 6, 7, 8 |
|
Взаимодействие join_view с другими адаптерами диапазонов открывает широкие возможности для создания сложных цепочек преобразований. Особенно полезна комбинация с transform_view:
| C++ | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
| struct User {
std::string name;
std::vector<std::string> roles;
};
std::vector<User> users = {
{"Alice", {"Admin", "Developer"}},
{"Bob", {"User"}},
{"Charlie", {"Developer", "Tester"}}
};
// Получение всех ролей всех пользователей
auto all_roles = users
| std::views::transform([](const User& user) { return user.roles; })
| std::views::join;
// Подсчет уникальных ролей
std::set<std::string> unique_roles(all_roles.begin(), all_roles.end()); |
|
Интересное применение join_view можно найти при работе с графовыми структурами. Например, для обхода графа в ширину (BFS):
| 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
| struct Node {
int id;
std::vector<Node*> neighbors;
};
auto bfs_traversal(Node* start) {
std::queue<Node*> queue;
std::set<Node*> visited;
queue.push(start);
visited.insert(start);
return std::views::generate([queue, visited]() mutable -> std::optional<Node*> {
if (queue.empty()) return std::nullopt;
Node* current = queue.front();
queue.pop();
for (auto* neighbor : current->neighbors) {
if (visited.insert(neighbor).second) {
queue.push(neighbor);
}
}
return current;
})
| std::views::take_while([](auto opt) { return opt.has_value(); })
| std::views::transform([](auto opt) { return *opt; });
} |
|
В заключение этого раздела стоит упомянуть о потенциальных ошибках при использовании join_view. Наиболее распространенные из них:
1. Изменение базовых диапазонов во время итерации. Если базовые последовательности изменяются во время итерации по join_view, поведение может быть непредсказуемым.
2. Предположение о произвольном доступе. join_view не поддерживает операцию operator[] и не обеспечивает произвольный доступ к элементам.
3. Игнорирование времени жизни базовых диапазонов. join_view не владеет данными, а лишь ссылается на них, поэтому базовые диапазоны должны оставаться действительными на протяжении всего времени использования join_view.
4. Лишние копии при неоптимальном использовании. Хотя сам join_view ленив и не создает копий, неосторожное использование может приводить к ненужным копиям.
| C++ | 1
2
3
4
5
6
7
8
| // Неоптимально: создается временный вектор
std::vector<int> temp_vec(std::views::join(nested).begin(),
std::views::join(nested).end());
// Лучше: обработка элементов непосредственно
for (int value : std::views::join(nested)) {
process(value);
} |
|
join_view — это мощный инструмент в арсенале C++ разработчика, особенно при работе со сложными вложенными структурами данных. Его ленивый характер и интеграция с другими компонентами библиотеки ranges делают его незаменимым для многих сценариев использования.
Возможности std::ranges::concat
В предыдущих разделах мы рассмотрели, как std::ranges::join_view позволяет сплющивать вложенные диапазоны. Однако нередко возникает необходимость объединять несколько отдельных диапазонов одного уровня вложенности. Именно для таких случаев в C++26 был введён новый компонент — std::ranges::concat_view.
В отличие от join_view, который работает с одним диапазоном диапазонов concat_view принимает произвольное количество независимых диапазонов и объединяет их в единую последовательность, не нарушая исходную структуру данных.
Ключевые характеристики concat_view:
1. Работает с несколькими независимыми диапазонами (а не с диапазоном диапазонов).
2. Поддерживает произвольный доступ, если все исходные диапазоны его поддерживают.
3. Позволяет модифицировать элементы, если базовые диапазоны допускают запись.
4. Выполняет ленивое вычисление без промежуточного выделения памяти.
Базовый синтаксис использования concat_view интуитивен:
| C++ | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
| #include <ranges>
#include <vector>
#include <string>
#include <print>
int main() {
std::vector<std::string> fruits{"яблоко", "груша"};
std::vector<std::string> vegetables{"морковь", "капуста"};
std::string colors[]{"красный", "зелёный", "синий"};
// Объединение трёх разных диапазонов
auto combined = std::views::concat(fruits, vegetables, colors);
for (const auto& item : combined) {
std::print("{} ", item);
}
// Вывод: яблоко груша морковь капуста красный зелёный синий
} |
|
Важное отличие от join_view заключается в том, что concat_view может напрямую объединять диапазоны разных типов и размеров. При этом не требуется, чтобы эти диапазоны заранее находились в каком-то контейнере более высокого уровня. Особенно ценной возможностью concat_view является поддержка произвольного доступа через оператор [], если все исходные диапазоны его поддерживают:
| C++ | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
| std::vector<int> v1{1, 2, 3};
std::vector<int> v2{4, 5, 6, 7};
std::array<int, 3> arr{8, 9, 10};
auto combined = std::views::concat(v1, v2, arr);
// Произвольный доступ работает
std::cout << combined[0] << std::endl; // 1 (первый элемент из v1)
std::cout << combined[3] << std::endl; // 4 (первый элемент из v2)
std::cout << combined[7] << std::endl; // 8 (первый элемент из arr)
// Модификация элементов
combined[0] = 100;
combined[5] = 200;
// Теперь v1[0] == 100 и v2[2] == 200 |
|
Эта возможность делает concat_view гораздо более гибким инструментом для многих сценариев, где требуется не только итерация, но и произвольный доступ к элементам объединённого диапазона.
Ещё одно важное преимущество concat_view — возможность комбинирования с другими адаптерами диапазонов, например, для применения трансформаций:
| C++ | 1
2
3
4
5
6
7
8
9
| std::vector<int> v1{1, 2, 3};
std::list<int> v2{4, 5, 6}; // Обратите внимание — здесь list, а не vector
// Трансформация каждого диапазона и последующая конкатенация
auto v1_squared = v1 | std::views::transform([](int x) { return x * x; });
auto v2_doubled = v2 | std::views::transform([](int x) { return x * 2; });
auto combined = std::views::concat(v1_squared, v2_doubled);
// Результат: 1, 4, 9, 8, 10, 12 |
|
В этом примере мы применяем разные трансформации к различным исходным диапазонам перед их объединением. Такой подход предоставляет большую гибкость при работе с гетерогенными данными. concat_view особенно полезен при работе со структурированными данными. Представим, что у нас есть система, где данные разделены на несколько категорий:
| 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
| struct Transaction {
std::string type;
double amount;
std::string currency;
};
int main() {
std::vector<Transaction> banking_transactions{
{"Deposit", 1000.0, "USD"},
{"Withdraw", 500.0, "USD"}
};
std::list<Transaction> credit_card_transactions{
{"Purchase", 123.45, "EUR"},
{"Refund", 50.0, "EUR"}
};
auto all_big_transactions = std::views::concat(
banking_transactions | std::views::filter([](const Transaction& t) {
return t.amount > 100.0;
}),
credit_card_transactions | std::views::filter([](const Transaction& t) {
return t.amount > 100.0;
})
);
for (const auto& t : all_big_transactions) {
std::print("{}: {} {}\n", t.type, t.amount, t.currency);
}
} |
|
В этом примере мы объединяем только транзакции определённого размера из разных источников, причём источники имеют разные типы контейнеров (vector и list).
При работе с текстовыми данными concat_view тоже предоставляет элегантные решения:
| C++ | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
| // Предположим, у нас есть несколько источников текстовых данных
std::vector<std::string> user_input{"Привет", "мир"};
std::array<std::string, 3> fixed_phrases{"Как", "дела", "?"};
std::string single_word = "Отлично";
// Объединяем всё в одну последовательность
auto message = std::views::concat(
user_input,
fixed_phrases,
std::views::single(single_word)
);
// Соединяем слова пробелами для формирования предложения
std::string sentence;
for (const auto& word : message) {
if (!sentence.empty()) sentence += " ";
sentence += word;
}
// Результат: "Привет мир Как дела ? Отлично" |
|
Обратите внимание на использование std::views::single для включения одиночного элемента в конкатенацию — этот приём часто оказывается полезным, когда нужно объединить диапазоны с отдельными элементами. Внутренняя реализация concat_view опирается на специальный итератор, который отслеживает текущую позицию в каждом из исходных диапазонов и выполняет переходы между ними при достижении конца. Схематично это можно представить так:
| C++ | 1
2
3
4
5
6
7
8
9
10
11
12
| template<typename... Ranges>
class concat_iterator {
private:
std::tuple<ranges::iterator_t<Ranges>...> iterators_;
std::tuple<ranges::sentinel_t<Ranges>...> sentinels_;
size_t current_range_index_ = 0;
// Методы для перемещения между диапазонами и доступа к текущему элементу...
public:
// Стандартный интерфейс итераторов...
}; |
|
С точки зрения производительности, concat_view обладает несколькими важными оптимизациями:
1. Для диапазонов с произвольным доступом операция operator[] имеет константную сложность O(1), так как может вычислить точный индекс в соответствующем исходном диапазоне.
2. Размер результирующего диапазона (если он известен) вычисляется как сумма размеров всех исходных диапазонов, что тоже делается за O(1).
3. Для случаев, когда все исходные диапазоны пусты, concat_view оптимизирован для быстрого возврата пустого диапазона без лишних вычислений.
Особый интерес представляет взаимодействие concat_view с constexpr контекстами. Начиная с C++20, многие компоненты std::ranges поддерживают вычисление во время компиляции, и concat_view не исключение:
| C++ | 1
2
3
4
5
6
7
8
9
10
| constexpr std::array<int, 3> arr1{1, 2, 3};
constexpr std::array<int, 2> arr2{4, 5};
constexpr auto combined = std::views::concat(arr1, arr2);
// Этот код может выполняться на этапе компиляции
constexpr int first = combined[0]; // 1
constexpr int last = combined[4]; // 5
static_assert(first == 1 && last == 5, "Constexpr concat failed!"); |
|
Это открывает интересные возможности для метапрограммирования и оптимизации кода, поскольку многие операции можно выполнить уже на этапе компиляции. В случаях, когда приходится работать с большими наборами данных, concat_view демонстрирует преимущества ленивого подхода. Вместо создания целого нового контейнера, который потребовал бы выделения памяти для всех элементов, concat_view просто предоставляет "окно" для просмотра исходных данных. Это особенно ценно при обработке потоков данных или при разработке систем с ограниченными ресурсами, где снижение потребления памяти критически важно.
Одной из менее очевидных, но мощных возможностей concat_view является его способность работать с бесконечными диапазонами, если они находятся не в первой позиции:
| C++ | 1
2
3
4
5
6
7
8
9
10
11
12
| // Бесконечный диапазон натуральных чисел
auto natural_numbers = std::views::iota(1);
// Конечный диапазон отрицательных чисел
std::array<int, 3> negative_numbers{-3, -2, -1};
// Объединение: сначала конечный диапазон, затем бесконечный
auto combined = std::views::concat(negative_numbers, natural_numbers);
// Первые 6 элементов объединённого диапазона
auto first_six = combined | std::views::take(6);
// Результат: -3, -2, -1, 1, 2, 3 |
|
Такая комбинация конечных и бесконечных диапазонов может быть полезна при создании генераторов специализированных последовательностей.
Семантические различия между concat_view и join_view наиболее явно проявляются при обработке пустых последовательностей. Рассмотрим:
| C++ | 1
2
3
4
5
6
7
8
9
10
11
12
13
| // Пустые диапазоны
std::vector<int> empty1;
std::vector<int> empty2;
std::vector<int> v{1, 2, 3};
// Конкатенация пустых диапазонов
auto concat_result = std::views::concat(empty1, v, empty2);
// Результат: 1, 2, 3 (пустые диапазоны эффективно игнорируются)
// Для сравнения с join_view
std::vector<std::vector<int>> nested{empty1, v, empty2};
auto join_result = std::views::join(nested);
// Результат тоже: 1, 2, 3 |
|
Хотя результат одинаков, механика обработки различна: concat_view просто "склеивает" диапазоны последовательно, в то время как join_view фактически "распаковывает" вложенную структуру.
Это различие становится критически важным при создании сложных цепочек преобразований:
| C++ | 1
2
3
4
5
6
7
8
9
| // С join нам нужна вложенная структура
auto complex_join = std::vector<std::vector<int>>{{1, 2}, {3, 4}}
| std::views::join;
// С concat мы работаем с отдельными диапазонами напрямую
auto complex_concat = std::views::concat(
std::vector<int>{1, 2},
std::vector<int>{3, 4}
); |
|
Взаимодействие concat_view с другими адаптерами диапазонов открывает новые возможности для элегантного решения сложных задач. Рассмотрим реальный пример реализации алгоритма "скользящего окна" (sliding window):
| C++ | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
| // Создание скользящего окна размера n для диапазона
template<std::ranges::viewable_range Range>
auto sliding_window(Range&& range, size_t window_size) {
if (window_size == 0) return std::views::empty<std::ranges::range_value_t<Range>>();
auto size = std::ranges::distance(range);
if (size < static_cast<decltype(size)>(window_size))
return std::views::empty<std::ranges::range_value_t<Range>>();
return std::views::iota(0, size - window_size + 1)
| std::views::transform([r = std::forward<Range>(range), window_size](auto i) {
return r | std::views::drop(i) | std::views::take(window_size);
});
}
// Использование
std::vector<int> data{1, 2, 3, 4, 5};
auto windows = sliding_window(data, 3);
// Результат: [1,2,3], [2,3,4], [3,4,5] |
|
Этот шаблон можно комбинировать с concat_view для более сложных случаев, например, для параллельного сравнения нескольких скользящих окон из разных источников:
| C++ | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
| auto time_series1 = std::vector<double>{0.1, 0.2, 0.3, 0.4};
auto time_series2 = std::vector<double>{0.2, 0.3, 0.4, 0.5};
auto windows1 = sliding_window(time_series1, 2);
auto windows2 = sliding_window(time_series2, 2);
// Создаем пары соответствующих окон из обоих источников
auto window_pairs = std::views::zip(windows1, windows2);
// Анализируем корреляцию между окнами
for (const auto& [window1, window2] : window_pairs) {
// Например, вычисление корреляции Пирсона между окнами
// compute_correlation(window1, window2);
} |
|
Важно отметить, что при использовании concat_view необходимо учитывать некоторые ограничения:
1. Производительность произвольного доступа может деградировать для большого количества диапазонов, так как требуется определить, к какому исходному диапазону относится запрашиваемый индекс.
2. При изменении элементов через ссылки, полученные из concat_view следует помнить, что изменения отражаются в оригинальных диапазонах. Это может быть как полезной особенностью, так и источником ошибок.
| C++ | 1
2
3
4
5
6
7
8
| std::vector<int> v1{1, 2};
std::vector<int> v2{3, 4};
auto concat = std::views::concat(v1, v2);
concat[0] = 10; // Изменяет v1[0]
concat[2] = 30; // Изменяет v2[0]
// Теперь v1 = {10, 2}, v2 = {30, 4} |
|
Для эффективного использования concat_view с глубоко вложенными структурами данных можно применять рекурсивный подход:
| C++ | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
| // Рекурсивная генерация последовательности Фибоначчи
auto fibonacci(int n) {
if (n <= 0) return std::views::empty<int>();
if (n == 1) return std::views::single(0);
if (n == 2) return std::views::concat(std::views::single(0), std::views::single(1));
auto prev = fibonacci(n - 1);
auto last_two = prev | std::views::reverse | std::views::take(2) | std::views::reverse;
int next_value = 0;
for (auto v : last_two) next_value += v;
return std::views::concat(prev, std::views::single(next_value));
}
// Получение первых 8 чисел Фибоначчи
auto fib_seq = fibonacci(8);
// Результат: 0, 1, 1, 2, 3, 5, 8, 13 |
|
Практическое применение новых функций
Теоретические аспекты join, join_with и concat очень важны для понимания их возможностей, но истинная ценность этих инструментов раскрывается в практических сценариях. Рассмотрим, как интегрировать эти операции с существующими алгоритмами ranges и применять их в реальных задачах разработки.
Интеграция с алгоритмами std::ranges
Одно из главных преимуществ библиотеки ranges — возможность комбинировать различные компоненты в единые выразительные цепочки. Операции объединения диапазонов гармонично вписываются в эту экосистему:
| C++ | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
| std::vector<std::vector<int>> data{{1, 3, 5}, {2, 4, 6}, {7, 8, 9}};
// Найти максимальный элемент среди всех вложенных векторов
auto max_element = std::ranges::max(data | std::views::join);
// max_element == 9
// Отсортировать элементы всех векторов как единую последовательность
auto sorted = data
| std::views::join
| std::ranges::to<std::vector>();
std::ranges::sort(sorted);
// sorted == {1, 2, 3, 4, 5, 6, 7, 8, 9}
// Подсчитать количество элементов, удовлетворяющих условию
auto count_even = std::ranges::count_if(
data | std::views::join,
[](int x) { return x % 2 == 0; }
);
// count_even == 4 |
|
Интеграция с concat_view выглядит аналогично:
| C++ | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
| std::vector<int> v1{1, 3, 5};
std::vector<int> v2{2, 4, 6};
std::vector<int> v3{7, 8, 9};
// Найти минимальный элемент среди всех векторов
auto min_element = std::ranges::min(std::views::concat(v1, v2, v3));
// min_element == 1
// Проверить, что в объединённой последовательности есть определённое значение
bool has_value = std::ranges::any_of(
std::views::concat(v1, v2, v3),
[](int x) { return x == 6; }
);
// has_value == true |
|
Для обработки текстовых данных эти операции также предлагают элегантные решения:
| C++ | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
| std::vector<std::string> first_names{"John", "Mary", "Robert"};
std::vector<std::string> last_names{"Smith", "Johnson", "Williams"};
// Преобразование списка имён и фамилий в полные имена
auto full_names = std::views::zip(first_names, last_names)
| std::views::transform([](auto pair) {
auto [first, last] = pair;
return std::views::concat(
std::views::all(first),
std::views::single(' '),
std::views::all(last)
);
})
| std::views::transform([](auto name_view) {
return std::string(name_view.begin(), name_view.end());
});
// Результат: {"John Smith", "Mary Johnson", "Robert Williams"} |
|
Альтернативные подходы для C++20/23
Не все проекты могут немедленно перейти на C++26, поэтому полезно знать, как добиться похожей функциональности в C++20 или C++23:
| C++ | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
| // Эмуляция concat_view в C++20
template<std::ranges::input_range... Rs>
auto concatenate(Rs&&... ranges) {
using CommonType = std::common_reference_t<std::ranges::range_value_t<Rs>...>;
std::vector<CommonType> result;
auto append_range = [&result](auto&& range) {
for (auto&& elem : range) {
result.push_back(std::forward<decltype(elem)>(elem));
}
};
(append_range(std::forward<Rs>(ranges)), ...);
return result;
}
// Использование в C++20
std::vector<int> v1{1, 2};
std::list<int> v2{3, 4};
auto combined = concatenate(v1, v2); |
|
Важное отличие этого подхода от настоящего concat_view — он не ленив и создаёт новый контейнер. Для больших объёмов данных это может быть неоптимально.
В C++23 можно использовать более гибкий подход с помощью views::zip и views::join_with:
| C++ | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
| template<std::ranges::input_range... Rs>
auto concat_c23(Rs&&... ranges) {
// Создаём кортеж из диапазонов
auto ranges_tuple = std::make_tuple(std::forward<Rs>(ranges)...);
// Создаём индексную последовательность для доступа к элементам кортежа
auto indices = std::views::iota(0u, sizeof...(Rs));
// Преобразуем индексы в соответствующие диапазоны
auto indexed_ranges = indices
| std::views::transform([&ranges_tuple](auto i) {
return std::views::all(std::get<i>(ranges_tuple));
});
// Объединяем все диапазоны
return indexed_ranges | std::views::join;
}
// Использование в C++23
auto combined = concat_c23(v1, v2, v3); |
|
Это решение ближе к настоящему concat_view, так как оно ленивое, но имеет более сложную реализацию и может быть менее эффективным.
Кастомизация через концепты
Библиотека ranges построена вокруг концептов, что делает её чрезвычайно гибкой для расширения. Мы можем создавать собственные адаптеры, которые комбинируют существующую функциональность:
| C++ | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
| // Адаптер для объединения диапазонов с преобразованием
template<std::ranges::input_range R, typename UnaryFunc>
auto join_transform(R&& range, UnaryFunc func) {
return std::forward<R>(range)
| std::views::transform(func)
| std::views::join;
}
// Адаптер для объединения диапазонов с фильтрацией
template<std::ranges::input_range R, typename Pred>
auto join_filter(R&& range, Pred pred) {
return std::forward<R>(range)
| std::views::transform([&pred](auto&& inner_range) {
return inner_range | std::views::filter(pred);
})
| std::views::join;
}
// Использование
std::vector<std::vector<int>> nested{{1, -2, 3}, {-4, 5, -6}};
auto positive_numbers = join_filter(nested, [](int x) { return x > 0; });
// Результат: {1, 3, 5} |
|
Такие кастомизированные адаптеры могут значительно упростить распространённые шаблоны использования и сделать код более читаемым.
Применение в многопоточных сценариях
Операции объединения диапазонов особенно полезны при работе с многопоточными вычислениями, когда результаты из разных потоков нужно объединить:
| 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
| // Параллельная обработка данных с последующим объединением результатов
std::vector<int> large_data(1000000);
// Заполняем данными...
// Разделяем данные на части для параллельной обработки
const size_t num_threads = std::thread::hardware_concurrency();
const size_t chunk_size = large_data.size() / num_threads;
std::vector<std::vector<int>> results(num_threads);
std::vector<std::thread> threads;
// Запускаем параллельную обработку
for (size_t i = 0; i < num_threads; ++i) {
threads.emplace_back([&, i]() {
auto start = large_data.begin() + i * chunk_size;
auto end = (i == num_threads - 1) ? large_data.end() : start + chunk_size;
// Обработка части данных
std::vector<int> thread_result;
for (auto it = start; it != end; ++it) {
if (*it % 3 == 0) { // Некоторое условие
thread_result.push_back(*it * 2);
}
}
results[i] = std::move(thread_result);
});
}
// Ждем завершения всех потоков
for (auto& thread : threads) {
thread.join();
}
// Объединяем результаты из всех потоков
auto combined_results = results | std::views::join;
// Используем объединенные результаты
for (int value : combined_results) {
// Обработка результатов...
} |
|
В этом примере мы эффективно распараллеливаем обработку данных, а затем используем join_view для объединения результатов без дополнительных копирований.
Заключение
Сравнивая новые возможности ranges с традиционными подходами, можно выделить несколько ключевых преимуществ:
Во-первых, сокращение объема кода. То, что раньше требовало десятков строк с вложенными циклами, временными переменными и ручным управлением итераторами, теперь можно выразить в одной-двух строках с использованием операций конвейера. Например:
| C++ | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
| // Традиционный подход
std::vector<int> result;
for (const auto& outer : nested_data) {
for (const auto& inner : outer) {
if (inner % 2 == 0) {
result.push_back(inner * 2);
}
}
}
// Современный подход с ranges
auto result = nested_data
| std::views::join
| std::views::filter([](int x) { return x % 2 == 0; })
| std::views::transform([](int x) { return x * 2; })
| std::ranges::to<std::vector>(); |
|
Во-вторых ленивость вычислений. Адаптеры диапазонов не выполняют немедленного преобразования данных, а создают "рецепт" обработки, который применяется только при фактическом доступе к элементам. Это позволяет избежать ненужных промежуточных копий и может значительно снизить потребление памяти при работе с большими наборами данных.
В-третьих, повышенная безопасность и меньшая подверженность ошибкам. Декларативный стиль, предлагаемый библиотекой ranges, избавляет от многих типичных ошибок, связанных с индексацией, границами массивов и незавершенными итерациями.
С точки зрения производительности, современные операции объединения диапазонов демонстрируют хорошие результаты. Внутренние оптимизации, такие как избегание промежуточных копий и эффективное управление состоянием итераторов, часто приводят к производительности, сравнимой или превосходящей традиционные императивные подходы.
При сравнении с популярными библиотеками range-v3 и boost::range стоит отметить, что стандартная библиотека C++ заимствовала многие идеи и проектные решения из этих проектов, особенно из range-v3. Однако существуют некоторые различия:
1. Range-v3 предлагает более широкий набор адаптеров и алгоритмов, чем текущая стандартная библиотека.
2. Boost.Range имеет другой подход к дизайну API, который может быть более удобным в определенных сценариях.
3. Стандартные компоненты часто имеют более строгие требования к корректности типов и могут быть менее гибкими в некоторых краевых случаях.
По мере взросления стандартной библиотеки ranges, разрыв в функциональности между ней и сторонними библиотеками постепенно сокращается. Компоненты, добавленные в C++26, такие как concat_view, ещё больше уменьшают необходимость в сторонних решениях.
Говоря о перспективах развития, можно выделить несколько направлений:
1. Улучшение интеграции с параллельными алгоритмами. В будущих стандартах вероятно появление более тесной интеграции между библиотекой ranges и параллельными алгоритмами, что позволит создавать ещё более эффективные и выразительные цепочки обработки данных.
2. Расширение набора представлений и адаптеров. Некоторые полезные компоненты из range-v3, такие как cartesian_product_view, stride_view или enumerate_view, могут быть включены в будущие стандарты.
3. Улучшение производительности. По мере взросления реализаций компиляторов, можно ожидать дальнейших оптимизаций, которые сделают операции с ranges ещё более эффективными.
4. Развитие поддержки constexpr. Расширение возможностей вычислений на этапе компиляции для библиотеки ranges, что позволит перенести ещё больше работы с данными из рантайма в компайл-тайм.
5. Интеграция с другими компонентами стандартной библиотеки, такими как контейнеры, строки и потоки ввода-вывода.
Эволюция библиотеки ranges демонстрирует общую тенденцию развития C++ в сторону более высокоуровневых абстракций и декларативного стиля программирования, при сохранении контроля над производительностью и эффективностью.
ошибка error: cannot convert 'std::string {aka std::basic_string<char>}' to 'std::string* {aka std::basic_stri на вод поступают 2 строки типа string. определить количество вхождений строки 2 в строку 1
ошибка... Написать программу сортировки заданных диапазонов чисел и заданных диапазонов символов подскажите пжлст с чего начать, осталась неделя до сдачи(
1 Написать интерактивную программу... Скорость ranges c++20 Насколько осторожно надо использовать объекты из этой библиотеки, например:
namespace stdr =... STL std::set, std::pair, std::make_pair Я не знаю как описать тему в двух словах, поэтому не обращайте внимание на название темы.... Не воспринимает ни std::cout, ни std::cin. Вобщем ничего из std. Также не понимает iostream Здравствуйте!
Я хотел начать изучать язык C++. Набрал литературы. Установил Microsoft Visual C++... (std::basic_string<char, std::char_traits<char>, std::allocator<char> > const& astxx::manager::connection::connection(std::basic_string<char, std::char_traits<char>,... Ошибка: E2034 Cannot convert 'int' to 'std::vector<std::vector<TRabbitCell,std::allocator<TRabbitCell>>... Есть двухмерный вектор:
std::vector<std::vector<TRabbitCell> > *cells(5, 10);
Пытаюсь... На основе исходного std::vector<std::string> содержащего числа, создать std::vector<int> с этими же числами подскажите есть вот такая задача.
Есть список .
Создать второй список, в котором будут все эти же... Std::begin() ,std::end(),std::copy ...//
int main()
{
std::vector<double> data;//Работает
cout << std::begin(data);
... Std::bind, std::mem_fun, std::mem_fn В чем разница между функциями std::bind, std::mem_fun, std::mem_fn? Std::unordered_multimap<std::string, std::unordered_multimap<int, int>> Приветствую. Интересует вопрос, как можно обращаться к контейнеру?
Хотелось бы по map, но так не... std::pair<std::list<std::pair< >>::iterator, > ломается при возврате из функции #include <iostream>
#include <list>
#include <string>
#include <utility>
using lp =...
|