Dwukierunkowe funkcje i obiekty proxy

2010-06-01 16:07

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ć:

  1. vector<int> v(10);
  2. v.at(5) = 25;

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:

  1. Dim S as String = "Ala ma kota"
  2. Mid(S, 4, 2) = "nie ma" ' Ala nie ma kota

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ącyproxy. 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:

  1. inline Mid_Proxy Mid(std::string& s, size_t off, size_t len)
  2.     { return Mid_Proxy(s, off, len); }

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:

  1. class Mid_Proxy
  2. {
  3.     private:
  4.         std::string& s;
  5.         size_t off, len;
  6.     public:
  7.         Mid_Proxy(std::string& _s, size_t _off, size_t _len)
  8.             : s(_s), off(_off), len(_len) { }
  9.  
  10.         void operator = (const std::string& str)
  11.             { s.replace (off, len, str); }
  12.         operator std::string () const
  13.             { return s.substr(off, len); }
  14. };

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:

  1. std::string s("Ala ma kota");
  2. Mid(s, 4, 2) = "nie ma";

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 stringa 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::stringiem, a jedynie dla const char* (co swoją drogą jest dość dziwne). Rozwiązaniem jest napisanie takowego dla samego obiektu proxy.

Be Sociable, Share!
Be Sociable, Share!
Tags: , , , , , ,
Author: Xion, posted under Programming »


6 comments for post “Dwukierunkowe funkcje i obiekty proxy”.
  1. Tomasz Dąbrowski:
    June 1st, 2010 o 18:57

    Hę? Lewy argument << to zawsze ostream&. Natomiast std::string nie ma implicit konwersji na const char*.

  2. Xion:
    June 1st, 2010 o 19:42

    /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()).

  3. Alton:
    June 1st, 2010 o 19:57

    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 ;)

  4. Alton:
    June 1st, 2010 o 19:58

    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?

  5. MySZ:
    June 1st, 2010 o 20:26

    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

  6. Reg:
    June 6th, 2010 o 19:28

    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.

Comments are disabled.
 


© 2023 Karol Kuczmarski "Xion". Layout by Urszulka. Powered by WordPress with QuickLaTeX.com.