Samodzielna nauka jest w wielu przypadkach bardzo ciekawym doświadczeniem, z co najmniej dwóch powodów:
- satysfakcja z udanej eksploracji nieznanych dotąd zasobów wiedzy.
- możliwość samodzielnego wyboru tempa i zakresu informacji opracowywanych podczas jednej „lekcji”.
Ma jednak jedną podstawową wadę – nikt poza uczniem nie weryfikuje poprawności przetwarzanych danych, a także ich zrozumienia. Dlatego czasem, pomimo tego, że nierzadko w wielu sprawach potencjalny uczeń ma rację, to i tak trzeba go „wyprostować”, wskazać inny, bardziej poprawny, sposób myślenia. Do czego zmierzam? Jak zwykle, chcąc podzielić się z innymi nieco wiedzą, do jednego z moich doświadczeń z przeszłości.
Moim jedynym „kwalifikowanym” kursem programowania były w tamtym czasie jedynie zajęcia informatyki w liceum, gdzie nauczyłem się całkiem nieźle programować w Pascalu i Delphi [tak, tak, wiem, Object Pascal - możesz już usiąść ;]]. Nie miałem więc okazji konsultować swojej wiedzy z kimś, kto w tej kwestii ma jakieś udokumentowane doświadczenie [Nie, tu w żadnym wypadku nie ma ofensywy w kierunku nauczyciela, po prostu wtedy nie miałem jeszcze pojęcia w którą stronę zawiedzie mnie morze moich zainteresowań, nie miałem też takich pomysłów jak teraz]. Dlatego też cały bagaż doświadczenia, jakie zdobyłem w pracy z językami innymi niż Pascal jest tylko i wyłącznie efektem czytania, pytania Wujka Google, pytania znajomych programistów, czytania, pytania Wujka Google, pytania znajomych programistów, itp., itd. Teraz nikogo nie powinno już dziwić, dlaczego wskazuję swoje, chociaż jak najbardziej niezamierzone, ale jednak błędy. Ku przestrodze dla potomnych.
Jako pierwszy wrzucam na stos sposób przekazywania zmiennych w parametrach funkcji / metod. Problem nie był irytujący, dopóki nie zaczął mnie interesować taki mały szkopuł jak szybkość działania kodu. Opiszę przypadek na przykładzie typu std::string, który, z powodu wielu operacji na łańcuchach znaków zaczął wywoływać u mnie wtedy dotkliwy ból głowy wyświetlając czas wykonania programu. Do tej pory nie widziałem nic złego w deklaracjach:
std::string retS(std::string str)
{
return str;
}
std::string str = retS("foo");
class Foo</pre>
{
private std::string foos;
public Foo(std::string x)
{
this->foos = x;
}
std::string getFoos(std::string str)
{
return str;
}
};
Foo *foo = new Foo("foos");
std::string foos = foo->getFoos();
Pewnego dnia zobaczyłem jednak w kodzie kolegi dziwny [wtedy] zapis:
const std::string&
Myślę sobie standardowo: WTF? Szybka konsultacja z autorem kodu i wszystko jasne – trzeba się przeuczyć. Teraz, kiedy już jestem mądrzejszy, mogę napisać jakie są zalety tego rozwiązania. Otóż:
- Pierwsze i najważniejsze, a jasne jak słońce: „&”, czyli operator referencji powoduje, że przesyłany jest jedynie adres zmiennej zamiast jej kopii lokalnej, tzn. nie zostaje wywołany konstruktor kopiujący i nie tracimy czasu i pamięci na utworzenie kopii dla miejsca docelowego użycia zmiennej. W tym przypadku po prostu nie będziemy kopiować stringa, żeby przekazać gdzieś drugi taki sam, ale damy mu gotowy obiekt.
- Drugie, nie mniej ważne: Jeśli zmienna nie będzie modyfikowana w miejscu, do którego jest przekazywana, dlaczego nie mielibyśmy nie ułatwić sobie pracy i nie nadać jej atrybutu const? W takim wypadku należy oszczędzić sobie czasu na szukanie błędu typu „dlaczego ten string zawiera jakieś śmieci, przecież nic z nim nie robiłem?!”, ponieważ kompilator automatycznie zaprotestuje przy każdej próbie naruszenia wartości tej zmiennej.
- Po trzecie, zachowując obydwie powyższe własności, w miejsce zmiennej można nadal wstawić literał, czyli używać jej w taki sam sposób jak wymienionego na początku „gołego” typu std::string.
Tak więc „poprawny” kod brzmi następująco:
std::string retS(const std::string& str)
{
return str;
}
std::string str = retS("foo");
class Foo</pre>
{
private std::string foos;
public Foo(const std::string& x)
{
this->foos = x;
}
std::string getFoos(const std::string& str)
{
return str;
}
};
Foo *foo = new Foo("foos");
std::string foos = foo->getFoos();
Słowo „poprawny” ująłem w cudzysłów, ze względu na to, że przekazywanie zmiennej w „zwykły” sposób nie jest niedozwolone, nie jest tez złą praktyką, ponieważ zawsze trzeba mieć na uwadze cel takiego działania. Jeśli potrzebujemy dwóch kopii zmiennej, to oczywiście nie podstawimy pod drugą referencji do pierwszego obiektu, tylko zainicjalizujemy ją jego wartościami. Opisuję to rozwiązanie dlatego, że w wielu przypadkach nie potrzebujemy jednak kopii i swobodnie możemy używać tej samej zmiennej, oszczędzając pamięć i skracając w dosyć znaczący sposób czas wykonania programu.
Mam nadzieję, że powyższe wyjaśnienie rozwiało większość wątpliwości dotyczących przekazywania zmiennych w kodzie. Chciałbyś coś dodać? Gdzieś się pomyliłem? Wiesz lepiej? Skomentuj ten wpis! ;]
Linki:
[1] http://pl.wikipedia.org/wiki/Literał – definicja „literału”.
[2] http://pl.wikipedia.org/wiki/Referencja_(informatyka) – Wikipedia o referencjach.
Warto przeczytać.
Trwa ładowanie…
Zgadzam się z tym. To może nie robić wielkiej różnicy przy małych obiektach, lub zmiennych typów wbudowanych (int, float, char), ale przy bardziej złożonych jest to naprawdę korzystne ze względu na wydajność. To jest także styl dobrego programowania i poleca je każdy autor książek do nauki programowania w tym języku. Polecam książki „Symfonia C++” i „Thinking in C++”. Jest tam bardziej szczegółowy opis referencji.
Faktycznie, moja konkluzja odnośnie wydajności nie była dość dobrze sprecyzowana, dzięki za poprawienie. Jeśli chodzi o książki, to Symfonię polecam każdemu, niezależnie od poziomu, ponieważ jest świetnie napisana i bardzo dobrze tłumaczy wszelkie zawiłości języka. Seria „Thinking in…” wydaje mi się odpowiednia jednak już dla osób, które wiedzą mniej więcej „o co chodzi”.
No i jeszcze można pomyśleć o wartości zwracanej, jeżeli możemy użyć referencji to nie wywołujemy konstruktora kopiującego na koniec działania funkcji. No ale nie zawsze jest to możliwe, bo nie możemy w ten sposób zwrócić zmiennej lokalnej funkcji. :P
Jeśli funkcja „produkuje” jakąś wartość to nie możemy użyć referencji, bo musimy zwrócić ten produkt bezpośrednio. W przypadku metod ogólnie nazywanych „getterami” mamy tą samą sytuację – zwykle nie chcemy, żeby ktoś za pomocą referencji modyfikował nam atrybuty obiektu, ponieważ do tego służy metoda „settera”, więc zwracamy kopię. Tak więc użycie referencji przy zwracaniu wartości jest dosyć odosobnionym przypadkiem – ja przypominam sobie tylko parę przypadków w których musiałem użyć tego typu konstrukcji, jedną z nich było przekazanie wektora zawierającego tysiące stringów. W tym wypadku kopiowanie nie byłoby zbyt szczęśliwym rozwiązaniem.
Problem ze zwracaniem referencji do zmiennych lokalnych rozwiąże przygotowywana w standardzie C++09 konstrukcja tzw. prawostronnych referencji [ang. rvalue references] według której czas życia zmiennej będzie przedłużony aż do ostatniego miejsca użycia, zamiast końca przestrzeni [bloku] w którym została zadeklarowana.