В архиве три файла для C++ (требуется поддержка компилятором C++11):
1. Qt
2. Visual C++
3. C++Builder
Версия для Visual C++ является базовой. В версии для Qt добавлена поддержка строковых литералов для QString и работа с QList. В версии для C++Builder добавлена поддержка строковых литералов для UnicodeString.
Реализация динамического массива на C++, обеспечивающего стабильность указателей.
Под стабильностью указателей понимается неизменность адресов в памяти, где располагаются объекты, являющиеся элементами массива. Т.е. указатель-переменная, созданный отдельно от массива и указывающий на любой его элемент, сохранит актуальность независимо от того, какие действия производятся с этим массивом.
Немного об идее.
Идея родилась по ходу разработки конкретного кода, где использовались указатели на элементы динамического массива std::vector и, соответственно, потребовалось сохранять валидность этих указателей при изменении размера вектора. А vector так не умеет. Стал смотреть альтернативы - std::list стабилен, но не имеет индексации - поэтому сразу в топку, std::deque работает через чанки, т.е. разрозненные последовательности элементов, но стабильности в нем нет, там тоже бывают реаллокации. Ну. и в C++Builder, в котором проект я разрабатывал, своих нормальных вариантов тоже нет, TList просто недоразумением показался (C++Builder 6 еще был со старым TList, хотя и новый не лучше). Так вот зародилась идея сделать динамический массив стабильных элементов. Ну, а потом еще выяснилось, что в Qt, на котором я писал программы раньше, начиная с 6 версии фактически ликвидирован класс QList, который превратился в мурзилку для QVector. Суть такая, что в Qt 5 для всех объектов, кроме простых типов, QList как раз и работал без реаллокации элементов, котрые хранились просто в "куче", а для для всеми любимых realloc существал QVector. Потом кто-то из разработчиков Qt изучил тысячи строк кода и внезапно обнаружил, что многие используют QList там, где следовало бы применить QVector, и не нашел ничего лучше как просто превратить QList в QVector. Так что для Qt 6, если вдруг потребуется динамический массив без реаллокаций, то тоже можно применять класс nx_array.
Не имея желания изобретать велосипед, я взял за основу стандартные std::list и std::vector. Вся идея очень проста: у класса nx_array два поля данных: data_list[] типа std::list и ptr_vector[] типа std::vector. Элементы массива статично лежат в data_list[], т.е. в "куче", без реаллокаций, а ptr_vector[] хранит указатели на эти элементы. Через эти указатели и реализован доступ по индексу.
Немного о реаллокациях и стабильности указателей.
Как уже было сказано, указатель-переменная, созданный отдельно от массива и указывающий на любой его элемент, сохранит актуальность независимо от того, какие действия производятся с этим массивом. Однако, это не означает, что элемент массива не может быть удален или перемещен с изменением адреса, что неминуемо приведет к инвалидации указателя. И, само собой, это не означает, что указатель будет как-либо привязан к индексации массива. Например:
nx_array<QString> my_array; my_array += "B"; // добавили элемент my_array[0] со значением "B" QString* item_ptr = &(my_array[0]); // item_ptr хранит адрес my_array[0]
// добавляем в массив новый my_array[0]:
my_array.addItem("A", 0); // item_ptr хранит прежний адрес, который теперь указывает на my_array[1]
Методы класса.
Методы nx_array условно разделяются на три категории:
1. Собственные методы класса, необходимые для комфортной работы. Эти методы отличаются своими названиями, записываемыми без символа подчеркивания, где каждое следующее слово пишется с заглавной буквы, например, метод добавления элемента - addItem().
2. Реализация некоторых стандартных методов STL, дающая возможность при желании быстро заменить в готовой программе объекты std::vector на nx_array, не исправляя вызовы методов класса. Речь идет о всем знакомым методах push_back(), pop_back() и т.д.
3. Методы, являющиеся общими для nx_array и STL, они, всё по той же логики совместимости с std::vector, имеют названия из STL: например, функция, возвращающая размер массива, т.е. количество его элементов, называется size(), а функция возвращающая ссылку на элемент массива с проверкой индекса - at(index).
Конструкторы класса
1. Конструктор без параметров и конструктор с параметрами размера и зарезервированного пространства (резервирование относится только к внутреннему массиву ptr_vector, а не к основному хранилищу объектов):
nx_array<int> arr1; // пустой массив nx_array<int> arr2(5); // массив с 5 элементами nx_array<int> arr3(3, 10); // 3 элемента, резерв = 10
2. Конструктор из std::initializer_list:
nx_array<int> arr1{1, 2, 3}; // массив с тремя элементами nx_array<int> arr2({1, 2, 3}, 10); // резерв = 10
nx_array<QString> arr1{"a", "b", "c"}; // массив с тремя элементами nx_array<QString> arr2({"a", "b", "c"}, 10); // резерв = 10 nx_array<QString> arr3{{"a", "b", "c"}, 10}; // тоже самое
3. Конструктор из rvalue-контейнера
nx_array<int> arr1(std::vector<int>{1, 2, 3}); // Перемещение из временного вектора nx_array<int> arr2(std::list<int>{4, 5, 6}); // Перемещение из временного спискаВ следующих конструкторах последний bool-параметр означает выбор между копированием (=false) и перемещением (=true):
4. Конструктор из контейнеров:
std::list<int> lst = {1, 2, 3}; nx_array<int> arr1(lst, 10); // Копирование из std::list, резерв = 10 nx_array<int> arr2(lst, 10, true); // Перемещение, резерв = 10
5. Конструктор из итераторов:
std::vector<int> vec = {1, 2, 3}; nx_array<int> arr1(vec.begin(), vec.end()); // копирование из вектора nx_array<int> arr2(vec.begin(), vec.end(), 10, true); // перемещение, резерв = 10
6. Конструктор копирования и перемещения:
nx_array<int> original{1, 2, 3}; nx_array<int> copy(original); // Копирование nx_array<int> moved(std::move(original)); // Перемещение nx_array<int> moved2(original, 0, true); // Явное перемещение
7. Конструкторы из конкретных контейнеров:
std::array<int, 3> std_arr = {1, 2, 3}; nx_array<int> arr_a(std_arr, 5); // Копирование, резерв = 5
std::vector<int> vec = {1, 2, 3}; nx_array<int> arr_v(vec, 10, true); // Перемещение, резерв = 10
8. Конструктор из простого массива:
int raw_arr[] = {1, 2, 3}; nx_array<int> arr1(3, raw_arr); // Копирование nx_array<int> arr2(3, raw_arr, 10, true); // Перемещение, резерв = 10
// Пример с std::vector: std::vector<int> vec = {4, 5, 6}; nx_array<int> arr3(3, vec.data()); // Копирование из данных вектора
Основные методы класса:
bool isPointerValid(const T* ptr) - ищет переданный в функцию указатель в векторе указателей класса. Несмотря на то, что указатели на элементы массива стабильны и в общем и целом можно без опасений создавать переменные-указатели на эти элементы, но указатель всё же потеряет актуальность в случае удаления элемента из массива. По этой причине реализована эта функция поиска элемента по его адресу.
int getItemIndex(const T* ptr) - по сути та же функция, но возвращает не признак того, найден элемент или нет, а его индекс. Если элемент отсутствует, вернет значение -1.
T* getItemPtr(size_type index) - возвращает адрес элемента массива по его индексу.
T& at(size_type index) - возвращает ссылку на элемент массива по его индексу (т.е. тоже самое, что оператор [index]) с проверкой корректности указанного индекса.
size_type size() - возвращает количество элементов массива
void checkIterator(const const_iterator& it) - функция проверки актуальности итератора
void moveItem(size_type from, size_type to) - перемещает элемент массива на другую позицию
void swapItems(size_type i, size_type j) - меняет два элемента массива местами
void removeItem(size_type index) - удаляет элемент массива по индексу
void removeRange(size_type start, size_type finish = END_POS) - удаляет диапазон элементов массива
void addItem(const T& value, int position = END_POS, bool useExc = false) - добавляет элемент в массив. По умолчанию - в конец массива. Если указан номер элемента, превышающий размер массива, то функция либо добавит элемент в конец (если useExc = false), либо выдаст исключение.
T& emplaceItem(int position = END_POS, Args&&... args) - добавляет элемент с инициализацией
void resize(size_type new_size) - меняет размер массива, т.е. либо удаляет лишние элементы, либо наоборот добавляет пустые элементы в конец массива
void resize(size_type new_size, const T& value) - тоже самое, но со значением по умолчанию для добавляемых элементов
void reverse() - меняет последовательность элементов массива на противоположную
bool sort() - сортирует элементы массива от меньшего к большему. Требует, чтобы типа данных массива поддерживал оператор "<", в противоположном случае функция вернет false
list_type toStdList() - возвращает копию массива в формате std::list
list_type ejectStdList() - возвращает (извлекает) оригинал объекта внутреннего поля std::list класса nx_array через list::splice(), обнуляя исходный массив. Адреса бывших элементов массива сохраняются неизменными, сторонние указатели не теряют актуальности.
list_type toStdList() && - вызывает ejectStdList()
std::vector<T> toStdVector() - возвращает копию массива в формате std::vector
std::vector<T> toStdVector() && - возвращает std::vector с перемещенными из std::list данными (через std::make_move_iterator), обнуляя исходный массив
std::deque<T> toStdDeque() - возвращает копию массива в формате std::deque
std::deque<T> toStdDeque() && - возвращает std::deque с перемещенными из std::list данными (через std::make_move_iterator), обнуляя исходный массив
QList<T> toQList() - возвращает копию массива в формате QList
QList<T> toQList() && - возвращает QList с перемещенными из std::list данными (через std::make_move_iterator), обнуляя исходный массив
void apply(Function func) - применяет функцию ко всем элементам массива
T extract(size_type index) - извлекает элемент (через std::move) с удалением из контейнера. Не работает с типами данных, которые нельзя перемещать
T extract(const_iterator pos) - метод, аналогичный предыдущему, но работающий через итератор, а не по индексу. Является аналогом метода extract() в реализации std::list стандарта C++17
void addArray(const Container& container, int position = END_POS) - присоединение другого массива путем копирования элементов. Поддерживаются массивы типа nx_array , vector, list, deque и QList
void addArray(const Container&& container, int position = END_POS) - присоединение другого массива путем перемещения элементов функцией std::move. Функция аналогичная методу mergeArray(), но требует указания std::move в явном виде при вызове. При вызове для типов данных nx_array или std::list вместо std::move будут вызваться функции spliceArray.
void addArray(const T* arr, size_type size, int position = END_POS, bool useExc = false) - функция, аналогичная предыдущей, но для добавления копии простого массива. Требует указания его размера. При useExc = true провоцирует исключения при указании нулевого адреса массива или некорректного параметра position
void spliceArray(list_type& lst, int position = END_POS) - присоединение массива std::list через list.splice, т.е. с сохранением присоединяемых элементов в исходных областях памяти. Указатели, созданные для этих элементов до выполнения функции, сохранят актуальность. Присоединяемый массив обнулится
void spliceArray(nx_array& arr, int position = END_POS) - метод, аналогичный предыдущему, но для nx_array . Поскольку nx_array хранит данные в std::list, то для него также доступно перемещение элементов через list.splice
void mergeArray(Container& container, int position = END_POS) - присоединение другого массива путем перемещения элементов функцией std::move. Поддерживаются массивы типа vector, deque и QList. Не используется для list и nx_array , поскольку для них существует более эффективная функция spliceArray()
Операторы, используемые в nx_array.
Как и в случае с функциями добавления элементов, операторы различают копирование и перемещение объектов через rvalue-ссылки (std::move) или функцией list.splice. Для присвоения копий элементов других классов-контейнеров используется оператор "=", а для добавления к существующим элементам таких копий - операторы "+" и "+=". Для присвоения другого контейнера путем перемещения его элементов используется оператор "<<=", для добавления элементов путем перемещения - "<<".
nx_array<QString> arr1 = {"X", "Y"};
nx_array<QString> arr2 = {"Z"};
arr1 += "A" + QString("B") << arr2; // arr1 = {X, Y, A, B, Z}
// arr2 - пуст
arr1 += ("A" + QString("B")) << arr2; // более корректная запись (учитывая, что << имеет более высокий приоритет, чем +)
Оператор [index] работает аналогично стандартному массиву, vector'у и т.п.
Методы, реализованные для удобной замены std::vector на nx_array.
Методы, возвращающие итераторы: begin(), end(), rbegin(), rend(), cbegin(), cend(), crbegin(), crend()
UPD
Добавлено публичное поле isDeleteItemsOnDestroy типа bool. Поле используется для массива, хранящего указатели, применительно к которым следует выполнить команды delete при удалении массива (объекта класса nx_array) из памяти. Поле isDeleteItemsOnDestroy немного схоже с свойством OwnObjects класса TList из Delphi и C++Builder. Однако, в отличие от OwnObjects оно управляет исключительно поведением деструктора ~nx_array(), а не каких-либо других методов класса. Если требуется выполнить команды delete применительно к удаляемым элементам динамического массива nx_array, которые являются указателями на объекты классов, то для этого специально реализованы отдельные методы, имеющие постфикс "_with_delete":
clear_with_delete()
iterator erase_with_delete(const_iterator pos)
removeItem_with_delete(size_type index)
removeRange_with_delete(size_type start, size_type finish = END_POS)
replace_with_delete(size_type index, T new_item)