Dzięki możliwości przeładowania sporej ilości operatorów (i to w szerokim zakresie) można robić dość ciekawe rzeczy.
Przykładowo, w C# składnia indeksera, czyli operatora subscriptu []
pozwala na rozróżnienie tego czy chcemy zapisać wartość czy ją odczytać. W C++ nie, bo możemy nim zwrócić tylko jedną wartość i zazwyczaj będzie to referencja do czegoś, jeżeli w ogóle chcemy umożliwić zapisywanie za jego pomocą. Ale można podejść do problemu z innej strony - jeżeli w C++ możemy przeładować operator przypisania to możemy zwrócić coś, co ten operator przeładowuje! Żeby umożliwić odczytanie, przeładujemy operator konwersji do naszego docelowego typu. Co więcej - możemy przyjąć i zwrócić co się nam podoba, więc paradoksalnie już robimy coś, czego C# nie umożliwia.
Załóżmy, że mamy pewnego rodzaju kontener, który musi przechowywać prototypy powiązane z kluczem. Chcielibyśmy przechowywać je w inteligentnym wskaźniku (nie chcemy sobie zawracać głowy ręcznym zwalnianiem pamięci po nich) i zwracać do nich stałe referencje (nie chcemy, żeby ktoś nam popsuł prototyp, a tym bardziej nie chcemy go oddawać - dlatego potrzebujemy zwrócić inny typ niż przyjmujemy). Naszym magicznym obiektem, o którym pisałem wcześniej będzie klasa Proxy
z odpowiednimi operatorami: przypisania i niejawnej konwersji.
class Container
{
private:
std::map<char, std::unique_ptr<Element>> _elements;
public:
void AddElement(char key, std::unique_ptr<Element> element)
{
_elements[key] = std::move(element);
}
const Element& GetElement(char key) const
{
return *_elements.at(key);
}
class Proxy;
Proxy operator[](char key)
{
return Proxy(*this, key);
}
class Proxy
{
friend Container;
private:
Container& _container;
char _key;
Proxy(Container& container, char key) : _container(container), _key(key) { }
public:
void operator=(std::unique_ptr<Element> element)
{
_container.AddElement(_key, std::move(element));
}
template <typename TElement>
void operator=(const TElement& element)
{
_container.AddElement(_key, std::unique_ptr<Element>(new TElement(element)));
}
operator const Element&()
{
return _container.GetElement(_key);
}
};
};
Dzięki takiej konstrukcji, możemy zrobić:
Container container;
container.AddElement('r', std::unique_ptr<Element>(new RedElement));
container['b'] = std::unique_ptr<Element>(new BlueElement);
container['g'] = GreenElement(); // to możliwe dzięki szablonom!
const Element& red = container.GetElement('r');
const Element& blue = container['b'];
Dzięki idiomowi z obiektem Proxy możemy zrobić jeszcze kilka interesujących rzeczy. Np. umożliwić taką konstrukcję na imitację list inicjalizacyjnych: obiekt = coś1, coś2, coś3
. Dzięki większemu priorytetowi operatora przypisania możemy zwrócić za jego pomocą obiekt Proxy, który będzie miał przeładowany operator przecinka. I to nawet dla różnych typów!
Warto w tym momencie zaznaczyć, że operatory są - i powinny - być tylko lukrem składniowym. Wszystko, co możemy zrobić za pomocą operatorów powinniśmy móc również zrobić za pomocą wywołań zwykłych metod.
A czy wy znacie jakieś ciekawostki tego typu :)?