Sztuczki i manipulowanie składnią

4

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 :)?

0

A czy wy znacie jakieś ciekawostki tego typu :)?
Zacznijmy od tego, że jedną z pierwszych rzeczy jakie poznajemy w C++ to użycie operatorów przesunięć bitowych do wysyłania i pobierania danych ze strumieni ;-)

Jednym ze straszniejszych zastosowań operatorów jest biblioteka Boost.Spirit. Wszystkie operatory są użyte do totalnie innych celów niż ich pierwotne znaczenie.

Innym przykładem (właściwie w C#, ale skoro .Net to i C++/CLI) może być Microsoft Solver Foundation. Tam co prawda arytmetyczne znaczenie poszczególnych operatorów jest zachowane, ale zamiast od razu zwracać wynik, zwracają obiekt reprezentujący samo równanie.

1

http://cpptk.sourceforge.net/
Emulowanie składni Tcl'a w C++.

2

Ciekawy temat, jest szansa że pojawi się tutaj kilka interesujących wypowiedzi...

A na temat - na przykład taka oto ciekawa implementacja lambd w C++ (co istotne, bez żadnych makr):
http://matt.might.net/articles/lambda-style-anonymous-functions-from-c++-templates/

lambda<int> (x) --> 3*x + 7 

Co jeszcze... Może takie oto przeciążanie przecinka (tak, najbardziej bezużyteczny operator C++ się do czegoś może przydać... albo i nie?)
http://xion.org.pl/2008/07/31/przeciazanie-przecinka/

std::vector<int> ints = (std::vector<int>(), 1, 2, 3, 4, 5);

I jeszcze z tego samego bloga, chyba najciekawszy (trochę się namęczyłem żeby to znaleźć - jedynka za złe otagowanie wpisu) - przypisywanie do substringa (pomysł skradziony przez Visual Basica):
http://xion.org.pl/2010/06/01/dwukierunkowe-funkcje-i-obiekty-proxy

Mid(s, 4, 2) = "nie ma";

Ogólnie inspirujące pytanie, jak będę miał jakiś pomysł to sam coś wymyślę.

0

@Rev, fajne rozwiązanie, ale czy w ten sposób do elementu kontenera nie trzeba odwoływać się zawsze przez casting?

tzn. widzę że można zrobić tak:

const Element& blue = container['b'];
s = blue.getHexValue();

ale co jeśli bym chciał zrobić tak:

s = container['b'].getHexValue() 

?

Nie mam przy sobie kompilatora, chyba to nie zadziała, co?

Jeśli tak, to niestety at, getAt, setAt nadal są potrzebne...

  • at() - jeśli mamy iteratory
  • getAt() / setAt() - jeśli nie

Edit: a co do innych przykładów to dla mnie największym wypasem jest to, że boost::function wreszcie pozwala zadeklarować delegata w normalny sposób. Nie mam pojęcia jak to zrobili, ale naprawili coś co było sp... od początku C:

boost::function<void (int values[], int n, int& sum, float& avg)> sum_avg;

void do_sum_avg(int values[], int n, int& sum, float& avg)
{
  sum = 0;
  for (int i = 0; i < n; i++)
    sum += values[i];
  avg = (float)sum / n;
}

sum_avg = &do_sum_avg;
0

Widziałem już nie jeden mechanizm emulujący własności w C++. Sam tez pisałem. Nie polecam. Pojawia się wiele ograniczeń i niewygód.

W twoim kodzie widzę, że zapamiętujesz wskaźnik na "rodzica" własności. Problem jest z tym taki, że:

  • utrudniona inicjalizacja, w konstruktorze trzeba inicjować wszystkie własności wskaźnikiem this
  • wskaźnik zajmuje dodatkowa pamięć
  • potrzebna dodatkowa dereferencja tego wskaźnika co zmniejsza efektywność (min. optymalizacja słabiej uprości kod)

Inne podejście to wykorzystanie szablonów i łączenie własności z właścicielami na etapie kompilacji. Rozwiązuje pewne problemy ale taż ma sporo wad. Komplikuje kod przede wszystkim i naraża na niespodziewane niespodziewajki.

Problemy z takimi własnościami związane są przede wszystkim z tym, że własność ma swoją klasę. Np.
cout << container['b']

W końcu końców doszedłem do wniosku, że nie należy stosować takich konstrukcji. Para metod setter+getter znacznie lepiej się nadaje. Keep it simple.

1 użytkowników online, w tym zalogowanych: 0, gości: 1