Я пытаюсь реализовать собственный класс контейнера карт, который использует древовидную структуру данных AVL. У меня есть все основные функции, и все работает нормально. Но когда я пытаюсь создать необходимые перегруженные функции, чтобы сделать мой контейнерный класс совместимым с циклами C++ с диапазоном for, кажется, что перегрузка оператора предварительного приращения моего класса итератора (operator++
) не вызывается.
У меня есть класс шаблона Map<key_type, value_type>
, который сам по себе является контейнером, и класс шаблона Pair<key_type, value_type>
, который представляет собой просто удобную структуру данных, содержащую пары ключ-значение. Класс Pair
также выполняет функцию итератора для Map
.
Короче говоря, я пытаюсь имитировать поведение TMap
и TPair
Unreal Engine:
Итерация по TMaps аналогична TArrays. Вы можете использовать функцию ранжирования C++, помня, что типом элемента является TPair.
Пример кода ниже:
TMap<int32, FString> FruitMap;
for (const TPair<int32, FString>& Fruit : FruitMap)
{
FPlatformMisc::LocalPrint(*FString::Printf(TEXT("(%d, \"%s\")\n"), Fruit.Key, *Fruit.Value));
}
Вот моя реализация классов Pair
и Map
:
template<typename key_type, typename value_type>
class Pair
{
friend Map<key_type, value_type>;
public:
Pair(const key_type& key, const value_type& value) :
m_Key(key),
m_Value(value)
{
}
private:
Pair& operator++()
{
return (m_OwningMap != nullptr) ? m_OwningMap->GetNextPair(*this) : *this;
}
public:
key_type m_Key;
value_type m_Value;
private:
mutable Map<key_type, value_type>* m_OwningMap = nullptr;
};
template<typename key_type, typename value_type>
class Map // Ordered Map
{
friend Pair<key_type, value_type>;
private:
struct Node
{
Pair<key_type, value_type> m_Pair;
size_t m_Depth;
Node* m_ParentNode;
Node* m_LeftChildNode;
Node* m_RightChildNode;
};
public:
Map();
Map(const Map<key_type, value_type>& otherMap);
~Map();
Pair<key_type, value_type>* begin()
{
if (m_RootNode != nullptr)
{
Node* leftMostChildNode = m_RootNode;
while (leftMostChildNode->m_LeftChildNode != nullptr)
{
leftMostChildNode = leftMostChildNode->m_LeftChildNode;
}
return &leftMostChildNode->m_Pair;
}
return nullptr;
}
Pair<key_type, value_type>* end()
{
return nullptr;
}
private:
Pair<key_type, value_type>& GetNextPair(const Pair<key_type, value_type>& currentPair)
{
Node* currentNode = RecursivelyFind(m_RootNode, currentPair.m_Key);
ASSERT_OR_DIE(currentNode != nullptr, "Node is null. Check allocation logic.");
if (currentNode->m_RightChildNode != nullptr)
{
currentNode = currentNode->m_RightChildNode;
while (currentNode->m_LeftChildNode != nullptr)
{
currentNode = currentNode->m_LeftChildNode;
}
}
else
{
while (currentNode->m_ParentNode != nullptr && currentNode == currentNode->m_ParentNode->m_RightChildNode)
{
currentNode = currentNode->m_ParentNode;
}
currentNode = currentNode->m_ParentNode;
}
return currentNode->m_Pair;
}
private:
Node* m_RootNode;
};
А это пример тестового использования этого контейнера:
int main()
{
Map<int, float> myMap;
myMap.Add(Pair<int, float>(0, 1.0f));
myMap.Add(Pair<int, float>(33, 2.0f));
myMap.Add(Pair<int, float>(7, 3.0f));
myMap.Add(Pair<int, float>(47, 4.0f));
myMap.Add(Pair<int, float>(99, 5.0f));
myMap.Add(Pair<int, float>(61, 6.0f));
for (const Pair<int, float>& currentPair : myMap)
{
DebuggerPrintf("Key: %i, Value: %f\n", currentPair.m_Key, currentPair.m_Value);
}
return EXIT_SUCCESS;
}
Теперь это не компилируется, но происходит сбой во время выполнения (P.S.: Add()
— это просто функция Map, которую я здесь опустил для минимальной читабельности, она работает нормально).
Я поставил точку останова в функции перегрузки оператора предварительного приращения. Точка останова на самом деле никогда не достигает. Пользовательская перегрузка не вызывается. Вместо этого кажется, что указатель увеличивается. Точнее, может быть, указатель Pair
? А поскольку указатель увеличивается как целое число, на каждой итерации он указывает на неправильную память, и в конечном итоге я получаю нарушение доступа на чтение, поскольку он не указывает на правильную память:
Я уверен, что что-то не так с моей реализацией. Я не совсем уверен, где. Что я здесь делаю не так?
Моя цель здесь — не найти средство для достижения цели, поэтому я не ищу быстрого решения, такого как std::map
или библиотеки Boost. Я пытаюсь узнать, как различные структуры данных и контейнеры реализуются с использованием C++.
«Класс Pair также выполняет функцию итератора для карты».: Это не может работать. Итератор — это нечто совершенно отличное от элементов контейнера.
@user17732522 user17732522 Тогда как Unreal Engine это делает? Я знаю, что их движок настроен гораздо сложнее, чем мой, но я помню, что их TPair можно использовать в качестве итератора и элемента для их TMap.
@KomalShashank TMap
begin
и end
возвращают TRangedForIterator
или TRangedForConstIterator
, а не TPair<...,...>*
. (Объект, который вы связываете в цикле for с диапазоном, не является итератором, это элемент разыменованного итератора.)
@Yksisarvinen, я понимаю. Я должен попробовать исправить это в ссылке и посмотреть, сработает ли это.
@KomalShashank Ссылка тоже не работает. Как упоминалось в комментарии выше вашего, итератор должен иметь тип, отличный от типа элемента. Как минимум, тип должен удовлетворять требованиям, указанным в en.cppreference.com/w/cpp/named_req/Iterator и en.cppreference.com/w/cpp/named_req/InputIterator , но, возможно, также en.cppreference.com/w/cpp/named_req/ForwardIterator и en.cppreference.com/w/cpp/named_req/BidirectIterator , но не en.cppreference.com/w/cpp/named_req/RandomAccessIterator .
Эквивалентный цикл «без диапазона» похож на for (Pair<int, float>* p = myMap.begin(); p != myMap.end(); ++p) { const Pair<int, float>& currentPair = *p; ... }
. Это позволяет легче увидеть, что происходит.
Ваш Pair
не является итератором.
Вы используете Pair
как структуру хранения данных и как итератор.
В C++ итераторы — это отдельные объекты, которые представляют позиции внутри контейнер, и им необходимо поддерживать определенные операции, такие как приращение, разыменование и сравнение.
Вам следует создать отдельный класс Iterator
внутри вашего класса Map
для обработки обхода.
Отделив функциональность Iterator
от класса Pair
и реализовав необходимые операции итератора, ваш контейнер будет правильно работать с циклами C++ с диапазоном for. Итератор инкапсулирует логику обхода и заставляет ваш контейнер Map
вести себя как стандартный контейнер C++.
Небольшой момент: итераторы представляют позиции внутри последовательности; контейнеры — это один из способов управления последовательностями, но не единственный. Например: std::istream_iterator.
Хм... Понятно. Итак, в этом случае я могу выделить логику итератора в отдельный класс. Но как мне добиться цикла с диапазоном for в формате: for (const Pair<int, float>& currentPair : myMap)
? Или я неправильно помню, и Range-for никогда не принимает объект элемента при своей инициализации?
@KomalShashank Итератор должен перегрузить operator*()
(без аргументов), чтобы вернуть Pair&
или const Pair&
. Этого operator++
и operator !=
должно быть достаточно.
@Yksisarvinen Это действительно помогает. Спасибо. Теперь у меня есть отдельный класс Iterator
, вложенный в мой класс Map
, который имеет только необходимые перегруженные функции оператора итератора, а структура Pair
теперь представляет собой просто структуру данных, инкапсулирующую ключ-значение. Теперь он работает так, как задумано, с циклами range-for. Я отмечаю этот ответ как решение. Спасибо всем.
Ваши
begin()
иend()
возвращают указатель наPair
. Компилятор вызываетoperator++
по этому указателю.