Правильная перегрузка оператора [] (с++)

Передо мной стоит задача написать свой ассоциативный массив и перегрузить для него оператор индексирования []. Я его написал, выглядит перегрузка так:

template<class Key, class Data>
Data& AssociativeArray<Key, Data>::operator[](const Key& key)
{
    for (Pair& p : array)
    {
        if (p.key == key)
        {
            return p.value;
        }
    }

    // Если ключ не найден создаём новую пару
    Pair newPair = { key, Data{} };
    array.push_back(newPair);
    return array.back().value;
}

//для константного массива
template<class Key, class Data>
const Data& AssociativeArray<Key, Data>::operator[](const Key& key) const
{
    for (const Pair& p : array)
    {
        if (p.key == key)
        {
            return p.value;
        }
    }
    return Data{};
}

Думаю, мой вопрос уже очевиден: в моей реализации я ищу ключ, и если его нет - создаю новый пустой элемент с таким ключом. С одной стороны это удобно - я таким образом могу присваивать новое значение, как обычному массиву:

Arr["newKey"] = "new value";

но с другой, при попытке получить значение по несуществующему ключу

std::string str = Arr["newKey"];

он создаёт новый пустой, хотя не должен, а должен бросить ошибку.

Вот и вопрос: как сделать правильно, чтоб он кидал ошибку при получении значения, и создавал элемент при добавлении?

На всякий случай вот вся структура класса:

template<class Key, class Data>
class AssociativeArray
{
public:
    Data& operator[](const Key& key);
    const Data& operator[](const Key& key) const;
    template<class K, class D>
    friend std::ostream& operator<<(std::ostream& os, const AssociativeArray<K, D>& arr);

private:
    struct Pair
    {
        Key key;
        Data value;
    };
    std::vector<Pair> array;
};

template<class Key, class Data>
Data& AssociativeArray<Key, Data>::operator[](const Key& key)
{
    for (Pair& p : array)
    {
        if (p.key == key)
        {
            return p.value;
        }
    }

    // Если ключ не найден создаём новую пару
    Pair newPair = { key, Data{} };
    array.push_back(newPair);
    return array.back().value;
}

template<class Key, class Data>
const Data& AssociativeArray<Key, Data>::operator[](const Key& key) const
{
    for (const Pair& p : array)
    {
        if (p.key == key)
        {
            return p.value;
        }
    }
    return Data{};
}

template<class Key, class Data>
std::ostream& operator<<(std::ostream& os, const AssociativeArray<Key, Data>& arr)
{
    for (const auto& pair : arr.array)
    {
        os << "Key: " << pair.key << ", Value: " << pair.value << std::endl;
    }
    return os;
}

Ответы (2 шт):

Автор решения: NunOfIt

Как вариант — создание прокси-класса, который хранит результат поиска, и для него определить оператор присваивания и оператор приведения; один создает объект, если надо, второй бросает исключение.

AssociativeArray.hpp

#ifndef ASSOCIATIVE_ARRAY_HPP
#define ASSOCIATIVE_ARRAY_HPP

#define AA AssociativeArray<Key, Data>
#define AATemplate template<class Key, class Data>
#define AASubclassMethod(type, subclass) AATemplate type AA::subclass

#include <vector>
#include <iostream>

AATemplate
class AssociativeArray {
public:
    class DataHolder {
        Data* value_;

    public:
        DataHolder();
        DataHolder(const Data&);

        DataHolder(DataHolder&&);
        DataHolder(const DataHolder&);

        DataHolder& operator=(DataHolder&&);
        DataHolder& operator=(const DataHolder&);

        ~DataHolder();

        Data& value();
        const Data& value() const;

        void clear();
        bool empty() const;

        Data& operator=(const Data&);

        operator bool() const;

        template<typename NewType> operator NewType() const;

        Data* operator->();

        Data& operator*();
        const Data& operator*() const;

        void swap(DataHolder&);

    private:
        void check_empty() const;
    };

    class Pair {
        Key* key_;
        DataHolder data_;

    public:
        Pair();
        Pair(const Key&);
        Pair(const Key&, const Data&);

        Pair(Pair&&);
        Pair(const Pair&);

        Pair& operator=(Pair&&);
        Pair& operator=(const Pair&);

        ~Pair();

        Key& key();
        const Key& key() const;

        Data& value();
        const Data& value() const;

        DataHolder& data();
        const DataHolder& data() const;

        void clear();

        void swap(Pair&);
    };

    bool empty() const;
    size_t size() const;

    DataHolder& operator[](const Key&);
    const DataHolder& operator[](const Key&) const;

    template<typename K, typename D>
    friend std::ostream& operator<<(std::ostream&, const AssociativeArray<K, D>&);

private:
    std::vector<Pair> array;
};

/* :::::::::::::::: */
/* DataHolder Class */
/* :::::::::::::::: */

// ---------- //
// - public - //
// ---------- //

/* Constructors */

AASubclassMethod(,DataHolder)::DataHolder(): value_(nullptr) {}
AASubclassMethod(,DataHolder)::DataHolder(const Data& value): value_(new Data(value)) {}
AASubclassMethod(,DataHolder)::DataHolder(const AA::DataHolder& other): AA::DataHolder::DataHolder(*other.value_) {}
AASubclassMethod(,DataHolder)::DataHolder(AA::DataHolder&& other): AA::DataHolder::DataHolder() { other.swap(*this); }

/* Assignement Operators (input: DataHolder Class) */

AASubclassMethod(typename AA::DataHolder&, DataHolder)::operator=(const AA::DataHolder& other) {
    this->clear();
    value_ = new Data(*other.value);
    return *this;
}

AASubclassMethod(typename AA::DataHolder&, DataHolder)::operator=(AA::DataHolder&& other) {
    this->clear();
    other.swap(*this);
    return *this;
}

/* Destructor */

AASubclassMethod(,DataHolder)::~DataHolder() { this->clear(); }

/* get_value Functions */

AASubclassMethod(Data&, DataHolder)::value() { return *value_; }
AASubclassMethod(const Data&, DataHolder)::value() const { return *value_; }

/* Other Functions */

AASubclassMethod(bool, DataHolder)::empty() const { return bool(value_); }
AASubclassMethod(void, DataHolder)::clear() { delete value_; value_ = nullptr; }

/* Other Operators */

AASubclassMethod(Data&, DataHolder)::operator=(const Data& value) {
    this->clear();
    value_ = new Data(value);
    return this->value();
}

AASubclassMethod(template<typename NewType>, DataHolder)::operator NewType() const { check_empty(); return NewType(this->value()); }

AASubclassMethod(Data*, DataHolder)::operator->() { return value_; }
AASubclassMethod(Data&, DataHolder)::operator*() { return this->value(); }
AASubclassMethod(const Data&, DataHolder)::operator*() const { return this->value(); }

AASubclassMethod(void, DataHolder)::swap(AA::DataHolder& other) { std::swap(value_, other.value_); }

// ----------- //
// - private - //
// ----------- //

AASubclassMethod(void, DataHolder)::check_empty() const {
    if(this->empty())
        throw std::runtime_error("///");
}

/* :::::::::: */
/* Pair Class */
/* :::::::::: */

// ---------- //
// - public - //
// ---------- //

/* Constructors */

AASubclassMethod(,Pair)::Pair(): key_(nullptr) {}
AASubclassMethod(,Pair)::Pair(const Key& key): key_(new Key(key)) {}
AASubclassMethod(,Pair)::Pair(const Key& key, const Data& value): key_(new Key(key)), data_(value) {}

AASubclassMethod(,Pair)::Pair(Pair&& other): Pair() { other.swap(*this); }
AASubclassMethod(,Pair)::Pair(const Pair& other): Pair(other.key(), other.value()) {}

/* Assignement Operators (input: Pair Class) */

AASubclassMethod(typename AA::Pair&, Pair)::operator=(Pair&& other) {
    this->clear();
    other.swap(*this);
    return *this;
}

AASubclassMethod(typename AA::Pair&, Pair)::operator=(const Pair& other) {
    this->clear();
    key_ = new Key(other.key());
    data_ = AA::DataHolder(other.data());
    return *this;
}

/* Destructor */

AASubclassMethod(,Pair)::~Pair() { this->clear(); }

/* get_value Functions */

AASubclassMethod(Key&, Pair)::key() { return *key_; }
AASubclassMethod(const Key&, Pair)::key() const { return *key_; }

AASubclassMethod(Data&, Pair)::value() { return *data_; }
AASubclassMethod(const Data&, Pair)::value() const { return *data_; }

AASubclassMethod(typename AA::DataHolder&, Pair)::data() { return data_; }
AASubclassMethod(const typename AA::DataHolder&, Pair)::data() const { return data_; }

/* Other Functions */

AASubclassMethod(void, Pair)::clear() { delete key_; key_ = nullptr; data_.clear(); }

AASubclassMethod(void, Pair)::swap(AA::Pair& other) {
    data_.swap(other.data_);
    std::swap(key_, other.key_);
}

/* :::::::::::::::::::::: */
/* AssociativeArray Class */
/* :::::::::::::::::::::: */

// ---------- //
// - public - //
// ---------- //

AATemplate bool AA::empty() const { return array.empty(); }
AATemplate size_t AA::size() const { return array.size(); }

AATemplate
typename AA::DataHolder& AA::operator[](const Key& key) {
    for (Pair& p: array)
        if (p.key() == key) { return p.data(); }

    // Если ключ не найден создаём новую пару
    array.push_back(Pair(key));
    return array.back().data();
}

AATemplate
const typename AA::DataHolder& AA::operator[](const Key& key) const {
    for (const Pair& p: array)
        if (p.key() == key) { return p.data(); }
    throw std::runtime_error("///");
}

AATemplate
std::ostream& operator<<(std::ostream& out, const AA& arr) {
    out << '{';
    if(!arr.empty()) {
        auto print_pair = [&out, &arr](size_t idx) -> char {
            out << '{' << arr.array[idx].key() << ", ";
            out << arr.array[idx].value() << '}';
            return '\0';
        };

        (void) print_pair(0);
        for(size_t i = 1; i < arr.size(); ++i) 
            out << ", " << print_pair(i);
    }
    out << '}';
    return out;
}

#endif

main.cpp

#include <string>
#include "AssociativeArray.hpp"

int main() {
    AssociativeArray<int, std::string> arr;

    arr[5] = "abc";
    arr[6] = "def";
    arr[-100] = "g";
    std::cout << arr << '\n';
    std::string str = arr[6];
    return 0;
}

Вывод:

{{5, abc}, {6, def}, {-100, g}}
terminate called after throwing an instance of 'std::runtime_error'
  what():  ///
zsh: IOT instruction  ./app
→ Ссылка
Автор решения: HolyBlackCat

Это невозможно нормально сделать. Самое близкое - это возвращать из [] по значению вспомогательный объект, с перегруженным operator= и operator T. Но оператор преобразования не позволит писать, например, вот так: foo[42].bar() - поэтому пришлось бы добавлять функцию для явного чтения элемента, в духе foo[42].get().bar(). Но это выглядит не очень красиво.


Вместо того, чтобы пытаться впихнуть два разных поведения в один [], лучше просто сделать две отдельных функции/оператора.

Например, оставить [] для чтения (и кидать исключение если элемента нет), а для вставки - отдельную функцию.

→ Ссылка