Jedną z pierwszych rzeczy poznawanych w trakcie nauki programowania jest sposób działania funkcji. Mam tu na myśli zupełnie elementarny fakt, iż funkcje przyjmują zero lub więcej argumentów na wejściu i produkują co najwyżej jeden rezultat na wyjściu. Niezachwiana pewność w tę własność funkcji może być potem mocno nadwątlona, jeśli poznamy języki o bardziej egzotycznym sposobie działania, jak na przykład Prolog; tam wszystkie parametry funkcji mogą przekazywać dane w obu kierunkach, dzięki czemu np. za dzielenie i łączenie ciągów lub list odpowiada jedna i ta sam funkcja.
Nie trzeba jednak uciekać tak daleko od starego dobrego programowania imperatywnego, żeby zaobserwować odstępstwa od reguły. Parametry mogą przecież służyć do zwracania wartości – dzieje się to przy pomocy słów kluczowych ref
/out
w C# lub zwykłych wskaźników/referencji w C++. Często zdarza się wtedy, że “normalny” rezultat zwracany przez funkcję służy jedynie przekazaniu informacji o ewentualnym błędzie.
Rzadsza sytuacja polega na tym, że wynik funkcji staje się jej argumentem wejściowym. Technicznie nie jest nawet możliwe, ale można tak traktować sytuacje, gdy rezultatem jest l-wartość (l-value), tj. obiekt, do którego można przypisywać:
Typowo jest to referencja (tutaj int&
), a powyższa konstrukcja – poprzez swojej podobieństwo do indeksowania zwykłej tablicy – nie należy do wielce zaskakujących. Co jednak można powiedzieć o tej:
poza widocznym od razu faktem, że pochodzi ona z języka, którego rozwlekłość składni przyprawia o niestrawność? :) Mianowicie tutaj nie ma już oczywistych przesłanek co do tego, czym jest lewa strona. Widać bowiem, że możemy jej przypisać dłuższy podciąg niż oryginalny i zostanie on mimo wszystko poprawnie zastąpiony – nie jest to więc żaden prosty “wyrób wskaźnikopodobny”. Może więc to po prostu feature akurat tego języka, którego nie da się zreplikować?…
Odpowiedź jest – jak można się domyślać, skoro o tym piszę – oczywiście negatywna :) Ażeby podobny efekt osiągnąć w C++, konieczne jest jednak zastosowanie techniki znanej jako obiekt pośredniczący – proxy. Powinien on zachowywać się jak zwykły rezultat funkcji, ale w razie potrzeby dawać również możliwość przypisywania do siebie. Naturalnie efekt takiego działania jest specyficzny dla konkretnej funkcji, która swoją drogą może być często zredukowana do samego tworzenia obiektu proxy:
Minimum, jakie rzeczony obiekt musi zapewniać, to operator przypisania oraz jakiś operator rzutowania, który pozwoli na “wyciągnięcie” rezultatu funkcji, gdy jest ona użyta w zwykły sposób:
Widzimy tutaj, że przy takim proxy nasza funkcja Mid
użyta w zwykły sposób zachowuje się jak metoda substr
. Gdy jednak umieścimy jej wywołanie po lewej stronie przypisania, zadziała metoda replace
, służąca do zastępowania podciągu innym. W sumie więc poniższy kod:
będzie działał analogicznie do prezentowanego wyżej fragmentu w języku Visual Basic.
Opisana tu sztuczka nie jest oczywiście doskonała. Do pełni możliwości potrzebna jest jeszcze wersja read-only funkcji Mid
, przyjmująca stałą referencję do string
a i wywołująca substr
bezpośrednio, z pominięciem obiektu proxy. To jednak da się łatwo zauważyć.
Mniej widoczny jest natomiast fakt, że obiekt proxy, dodając kolejną warstwę niejawnych konwersji (tutaj: z siebie na string
) może spowodować kłopoty tam, gdzie jedna niestandardowa konwersja jest już wykorzystywana. Dobry przykład to interakcja ze strumieniem:
std::cout << Mid(s, 0, 3) << std::endl;[/cpp]
która nie powiedzie, bo operator strumienowy <<
nie posiada wersji dla lewego prawego argumentu będącego std::string
iem, a jedynie dla const char*
(co swoją drogą jest dość dziwne). Rozwiązaniem jest napisanie takowego dla samego obiektu proxy.
Hę? Lewy argument << to zawsze ostream&. Natomiast std::string nie ma implicit konwersji na const char*.
/facepalm. Oczywiście chodziło mi o prawy argument. Nie wspominałem natomiast o niejawnej konwersji z std::string na const char*, bo jej faktycznie nie ma (trzeba uzyć c_str()).
Ależ żeby wypisywać stringi strumieniami nie trzeba żadnymi konwersjami się bawić. Wystarczy na początku programu dopisać magiczne #include. Tam już jest odpowiednio przeciążony operator i wszystko zgrabnie działać będzie ;)
Ten include to ma dołączać nagłówek string oczywiście. Z jakiegoś powodu wycięło mi tę nazwę z poprzedniego komentarza. To przez nawiasy trójkątne?
Och, całkiem jak w perlu ;)
my $str = ‘Ala ma kota’;
substr ($str, 4, 2) = ‘od’;
print $str, “\n”; # Ala od kota
Oczywiście samemu też można takie funkcje tworzyć:
sub mysubstr : lvalue {
substr ($_[0], $_[1], $_[2]);
}
mysubstr ($str, 4, 2) = ‘spod’;
print $str, “\n”; # Ala spod kota
Ciekawy temat poruszyłeś. Przypomniał mi interesującą koncepcję obiektów reprezentujących zakres elementów jakiejś kolekcji, taki jaki stosuje m.in. biblioteka Intel Threading Building Blocks (np. klasa blocked_range). To alternatywa dla podejścia z STL – wyzaczania zakresu za każdym razem za pomocą dwóch osobnych iteratorów.