Почему перегрузка оператора предварительного приращения не вызывается для моего пользовательского класса итератора в C++?

Я пытаюсь реализовать собственный класс контейнера карт, который использует древовидную структуру данных 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++.

Ваши begin() и end() возвращают указатель на Pair. Компилятор вызывает operator++ по этому указателю.

Yksisarvinen 16.08.2024 13:11

«Класс Pair также выполняет функцию итератора для карты».: Это не может работать. Итератор — это нечто совершенно отличное от элементов контейнера.

user17732522 16.08.2024 13:19

@user17732522 user17732522 Тогда как Unreal Engine это делает? Я знаю, что их движок настроен гораздо сложнее, чем мой, но я помню, что их TPair можно использовать в качестве итератора и элемента для их TMap.

Komal Shashank 16.08.2024 13:28

@KomalShashank TMapbegin и end возвращают TRangedForIterator или TRangedForConstIterator, а не TPair<...,...>*. (Объект, который вы связываете в цикле for с диапазоном, не является итератором, это элемент разыменованного итератора.)

molbdnilo 16.08.2024 13:34

@Yksisarvinen, я понимаю. Я должен попробовать исправить это в ссылке и посмотреть, сработает ли это.

Komal Shashank 16.08.2024 13:36

@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 .

user17732522 16.08.2024 13:38

Эквивалентный цикл «без диапазона» похож на for (Pair<int, float>* p = myMap.begin(); p != myMap.end(); ++p) { const Pair<int, float>& currentPair = *p; ... }. Это позволяет легче увидеть, что происходит.

molbdnilo 16.08.2024 13:40
Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
0
7
76
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

Ваш Pair не является итератором.

Вы используете Pair как структуру хранения данных и как итератор.

В C++ итераторы — это отдельные объекты, которые представляют позиции внутри контейнер, и им необходимо поддерживать определенные операции, такие как приращение, разыменование и сравнение.

Вам следует создать отдельный класс Iterator внутри вашего класса Map для обработки обхода.

Отделив функциональность Iterator от класса Pair и реализовав необходимые операции итератора, ваш контейнер будет правильно работать с циклами C++ с диапазоном for. Итератор инкапсулирует логику обхода и заставляет ваш контейнер Map вести себя как стандартный контейнер C++.

Небольшой момент: итераторы представляют позиции внутри последовательности; контейнеры — это один из способов управления последовательностями, но не единственный. Например: std::istream_iterator.

Pete Becker 16.08.2024 13:33

Хм... Понятно. Итак, в этом случае я могу выделить логику итератора в отдельный класс. Но как мне добиться цикла с диапазоном for в формате: for (const Pair<int, float>& currentPair : myMap)? Или я неправильно помню, и Range-for никогда не принимает объект элемента при своей инициализации?

Komal Shashank 16.08.2024 13:49

@KomalShashank Итератор должен перегрузить operator*() (без аргументов), чтобы вернуть Pair& или const Pair&. Этого operator++ и operator != должно быть достаточно.

Yksisarvinen 16.08.2024 14:31

@Yksisarvinen Это действительно помогает. Спасибо. Теперь у меня есть отдельный класс Iterator, вложенный в мой класс Map, который имеет только необходимые перегруженные функции оператора итератора, а структура Pair теперь представляет собой просто структуру данных, инкапсулирующую ключ-значение. Теперь он работает так, как задумано, с циклами range-for. Я отмечаю этот ответ как решение. Спасибо всем.

Komal Shashank 16.08.2024 21:46

Другие вопросы по теме

Похожие вопросы

Синтаксический сахар для синхронизации блока кода в C++
Как ограничить функцию шаблона определенными типами?
Visual Studio C++: копирование проектов и решений с одного компьютера на другой (github не работает)
Странное поведение кнопки EnableWindow при нажатии
Можно ли с уверенностью предположить, что 32-битные числа с плавающей запятой можно напрямую сравнивать друг с другом, если значение соответствует мантиссе?
Практическое руководство: функция C++, которая настраивает тип возвращаемого значения в соответствии с потребностями вызывающей стороны
Возвращает ли low_bound() один и тот же результат с обратными итераторами вектора в порядке возрастания и прямыми итераторами вектора в порядке убывания?
Почему std::make_format_args ожидает неконстантную ссылку
C++ Существует ли быстрый многомерный массив, который позволяет использовать подмассивы разного размера?
Необходим ли std::atomic или voluty при установке переменных из обработчика сигналов?