Правильная перегрузка оператора [] (с++)
Передо мной стоит задача написать свой ассоциативный массив и перегрузить для него оператор индексирования []. Я его написал, выглядит перегрузка так:
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 шт):
Как вариант — создание прокси-класса, который хранит результат поиска, и для него определить оператор присваивания и оператор приведения; один создает объект, если надо, второй бросает исключение.
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
Это невозможно нормально сделать. Самое близкое - это возвращать из [] по значению вспомогательный объект, с перегруженным operator= и operator T. Но оператор преобразования не позволит писать, например, вот так: foo[42].bar() - поэтому пришлось бы добавлять функцию для явного чтения элемента, в духе foo[42].get().bar(). Но это выглядит не очень красиво.
Вместо того, чтобы пытаться впихнуть два разных поведения в один [], лучше просто сделать две отдельных функции/оператора.
Например, оставить [] для чтения (и кидать исключение если элемента нет), а для вставки - отдельную функцию.