zwracanie wartości

0

Cześć :)
Mamy następującą funkcję:

vector<int> func() {
vector<int> Vec;
//wypelniamy go
return Vec;
}
int main(){
vector<int> vec2= func(); //1
func(); //2 
}

Pytanie do komenatarza nr 1:
Chciałbym dowiedzieć się jak odbywa się zwracanie wartości? Z tego co pamiętam, to zwracana wartość wędruje na stosie. Co dalej się z nią dzieje? Widać, że w pierwszym wywołaniu jej wartość zostaje zapamiętana, a w drugim nie. Jak to wygląda dla tych dwóch różnych sytuacji?

Pytanie drugie:

Program 1:
vector<int> func() {
vector<int> Vec;
//wypelniamy go
return Vec;
}
int main(){
vector<int> vec2= func(); 

}

Program 2:
vector<int> func() {
vector<int> Vec;
//wypelniamy go
return Vec;
}
int main(){

func(); 
}

Po disassemblowaniu kodu funkcji main() wygląda kod następująco:

 0x0000000000400682 <+0>:	push   %rbp
   0x0000000000400683 <+1>:	mov    %rsp,%rbp
   0x0000000000400686 <+4>:	sub    $0x20,%rsp
   0x000000000040068a <+8>:	lea    -0x20(%rbp),%rax
   0x000000000040068e <+12>:	mov    %rax,%rdi
   0x0000000000400691 <+15>:	callq  0x400664 <func()>
   0x0000000000400696 <+20>:	lea    -0x20(%rbp),%rax
   0x000000000040069a <+24>:	mov    %rax,%rdi
   0x000000000040069d <+27>:	callq  0x4006c4 <std::vector<int, std::allocator<int> >::~vector()>
   0x00000000004006a2 <+32>:	mov    $0x0,%eax
   0x00000000004006a7 <+37>:	leaveq 
   0x00000000004006a8 <+38>:	retq   

I to dla obydwu programów tak samo! Jak to możliwe, skoro w jednym wywołaniu to zapamiętujemy?

1

Wyłącz optymalizacje.

0

ok, wyłączę i wtedy popatrzę, co tu jest optymalizowane?

Proszę jeszcze o odpowiedź na 1. pytanie.

Skompilowałem z opcją
-O0
i asm wyglądają tak samo.

0

co tu jest optymalizowane?

Z tego co widzę, kompilator alokuje zmienną vec2 w rejestrze rax, dlatego ten kod wygląda identycznie.

Z tego co pamiętam, to zwracana wartość wędruje na stosie.

afair w przypadku większości platform (w tym x86, x86-64) najczęściej (zawsze?) zwracana wartość znajduje się w rejestrze.

0

Skompilowałem z opcją
-O0
i asm wyglądają tak samo.

Kompilator usunął drugie wywołanie func(), ponieważ 'nic nie robiło'.

  1. Ok, a jeżeli by go nie usunął to co się dzieje ze zwróconą wartością?
  2. Skoro usunął to wywołanie, to dlaczego kod asm. wygląda tak samo jak w pierwszym przypadku?
1
  1. Ok, a jeżeli by go nie usunął to co się dzieje ze zwróconą wartością?

Nic, pozostaje w rejestrze.

  1. Skoro usunął to wywołanie, to dlaczego kod asm. wygląda tak samo jak w pierwszym przypadku?

Mea culpa, źle spojrzałem na ten kod.

0

ok, wyłączyłem optymalizację ( -O0 ) jak kazał @_13th_Dragon i kody assemblerowe nadal się nie różnią.

1

Chciałbym dowiedzieć się jak odbywa się zwracanie wartości?
To zależy od systemu i od kompilatora. Sam standard języka C++ nie narzuca gdzie ma być przechowywana wartość zwracana przez funkcję.
Dobry kompilator z włączoną optymalizacją może ci utworzyć wektor bezpośrednio pod zmienną vec2 bez kopiowania.

I to dla obydwu programów tak samo! Jak to możliwe, skoro w jednym wywołaniu to zapamiętujemy?
Oba programy robią dokładnie to samo: tworzą wektor, a następnie go niszczą.

Nie ma znaczenia że zapisujesz wektor do zmiennej, skoro zaraz jest zamykający }, zmienna wypada z zasięgu i obiekt jest niszczony. Czyli dokładnie tak samo jak w przypadku gdy nie zapisujesz wyniku do zmiennej.

Tu nie ma znaczenia optymalizacja, po prostu oba programy są równoważne.

0

Tu nie ma znaczenia optymalizacja, po prostu oba programy są równoważne.

Jednak nie. Teraz, gdy wyłączyłem optymalizację copy ellision kody się różnią :)

unique_ptr<int> increment( unique_ptr<int> i )
{
    ++*i;
    return i;
}

Wg bloga, którego czytam zmienna i jest kopiowana. Ja jednak tego nie widzę. Jak to jest kopiowana?

0

Wywołanie konstruktorów w ASMie wygenerowanym masz, prawda?

tak, mam;

Nie, ponieważ w C++ instrukcja object x = ... nie jest przypisaniem do obiektu, tylko utworzeniem obiektu

hmm, czyli jeżeli mam utworzony jakiś już obiekt, np. obiekt a.
to taka operacja:
a =b, gdzie b jest obiektem tego samego typu co powoduje? Z tego co napisałeś, to rozumiem, że odbywa się konstrukcja nowego obiektu na podstawie wartości w b.
Ale dotychczas byłem w przekonaniu, że w takiej sytuacji jest wywoływany konstruktor kopiujący. Jak to w końcu jest?

2
Foo a=b;

nie jest tym samym co Foo a; a=b;

 zaś jest tym samym co <code class="cpp">Foo a(b);

poprzez działanie http://en.wikipedia.org/wiki/Copy_elision które przeważnie można wyłączyć w kompilatorze.

2
mielony napisał(a):

tego co napisałeś, to rozumiem, że odbywa się konstrukcja nowego obiektu na podstawie wartości w b.
Ale dotychczas byłem w przekonaniu, że w takiej sytuacji jest wywoływany konstruktor kopiujący. Jak to w końcu jest?

Licho, skasowałem, bo nie chciałem się wtrącać, ale za szybko przeczytałeś, albo ja za wolno skasowałem... ;)

Chodzi o to, że ten znak = jest mylący w C++.

// tutaj nie ma przypisania, tutaj rusza jakiś konstruktor. 
// zależnie od wartości X, to będzie konstruktor kopiujący, jakiś konwertujący
// albo i przenoszący, bo w C++11 takie coś wymyślili
object a = X
object a;

// tutaj jest przypisanie, tutaj nie ruszają konstruktory, tylko operator=, czyli przypisanie
// no dobra... może ruszyć tandem: konstruktor konwertujący, a potem dopiero operator przypisania
a = X;

To tylko uwaga na boku, żebyś to miał na uwadze, śledząc znaki =. Samego problemu (jak zwracana jest wartość) nie chcę ruszać, bo z pamięci nie powiem, kiedy co rusza, a a zanim znajdę, to ktoś znający standard na wyrywki pewnie zdąży wpaść ;P

2

C++ to język który zakłada możliwość radosnego kopiowania obiektów na prawo i na lewo — przy przypisaniu, przy zwracaniu wartości z funkcji, przy przypisaniu zwracanej wartości do zmiennej itp.
To jednak jest bez sensu, by tworzyć ciągle tymczasowe kopie obiektów tylko po to, by zaraz je wyrzucić.
Dlatego kompilator może (powinien!) wycinać zbędne kopiowanie - to jednak wydłuża czas kompilacji (bo trzeba głębiej zanalizować kod) i utrudnia debugowanie. Dlatego te i inne optymalizacje można włączyć albo wyłączyć.

Docelowy program powinien być oczywiście zoptymalizowany.
Nie oczekuj również, że kod nawet przy -O0 będzie „debilny” instrukcja w instrukcję.
Dobrze jest rozumieć jakie optymalizacje kompilator stosuje, ale nie powinno to się przeradzać w poszukiwanie parametrów dających najgorszy możliwy kod :-)

0

dalej pozostaje otwarte pytanie,
gdzie kompilator odkłada zwracaną wartość?
Padła odpowiedź, w rejestrze. Co jeżeli obiekt nie mieści się w rejestrze?

0

Co jeżeli obiekt nie mieści się w rejestrze?

Obiekt nie musi, a jego instancja jest wskaźnikiem, więc siłą rzeczy musi się zmieścić.

0

Dobrze, to w takim razie w świetle tego czym różni się przekazywanie przez wartość od przekazywania przez wskaźnik?

0

Em, no w jednym przypadku przekazujesz wartość, a w drugim referencję (wskaźnik) na wartość...

0

no w porządku, ale czym to się różni jeśli chodzi o kompilator

0

Jeśli chodzi o wygenerowany kod, to w jednym przypadku przekazujesz wartość, a w drugim wskaźnik na wartość. To naprawdę takie proste. I niczym się nie różni w odniesieniu do tego samego zdania sformułowanego w kontekście C++ (patrz wyżej).

Przekazywanie przez wartość (return 2*foo;):

lea	eax, [rdi+rdi]

Przez referencję (return 2*(*foo);):

mov	eax, DWORD PTR [rdi]
add	eax, eax

Jak widzisz, przekazując przez referencję dochodzi nam mov eax, DWORD PTR [rdi], czyli dereferencja wskaźnika, aby wyłuskać jego wartość - dopiero potem możemy na tej wartości operować.

0

Nie chodziło mi o przekazywanie przez wartość, a chodziło mi czym różni się zwracanie obiektu od zwracania wskaźnika do obiektu z punktu widzenia kompilatora.
Przepraszam za wprowadzenie w błąd ;)

0

Niczym się nie różni: zwracasz albo adres instancji obiektu, albo wskaźnik na ten adres.
Instancję obiektu możesz sobie wyobrazić jako int (adres - formalnie void*), a wskaźnik na to jako int* (void**).

0

W takim razie w jakim celu w ogóle istnieje takie coś jak Return Value Optimization?

3

Żeby zwrócić obiekt, ten obiekt musi gdzieś w pamięci istnieć - nawet jeśli na poziomie asemblera zwracany jest tylko wskaźnik.
Jeżeli funkcja zwraca obiekt będący zmienną lokalną, obiekt ten musi zostać przekopiowany w "miejsce na zwracany obiekt" - nie może pozostać tam gdzie jest zmienna lokalna, bo ten obszar stosu zostanie zniszczony w momencie wychodzenia z funkcji (zniszczony w sensie umownym. po prostu może być zamazany przez coś innego).
Dlatego (przy braku RVO) return powoduje skopiowanie obiektu ze zmiennej lokalnej do wartości zwracanej (czyli wywołanie konstruktora kopiującego), po czym zniszczenie zmiennej lokalnej (wywołanie destruktora).
RVO polega na tym, że zwracana zmienna zamiast być utworzoną w "miejscu na zmienne lokalne" jest tworzona w "miejscu na zwracany obiekt". Dzięki temu wypada jedno kopiowanie obiektu i jedna destrukcja.

Utwórz sobie najlepiej klasę

class Foo
{
    public:
        Foo() { cout << "Foo()" << endl; }
        ~Foo() { cout << "~Foo()" << endl; }
        Foo(const Foo&) { cout << "Foo(const Foo&)" << endl;}
        Foo& operator=(const Foo&) { cout << "Foo& operator=(const Foo&)" << endl; return *this; }
}

i kopiuj, zwracaj, sprawdź jak się zmienia zachowanie kodu w zależności od flag kompilacji.

PS. zadanie "z gwiazdką":

        Foo(Foo&&) { cout << "Foo(Foo&&)" << endl; }
        Foo& operator=(Foo&&) { cout << "Foo& operator=(Foo&&)" << endl; return *this; }
4
mielony napisał(a):

dalej pozostaje otwarte pytanie,
gdzie kompilator odkłada zwracaną wartość?
Padła odpowiedź, w rejestrze. Co jeżeli obiekt nie mieści się w rejestrze?

Patryk27 napisał(a):

Obiekt nie musi, a jego instancja jest wskaźnikiem, więc siłą rzeczy musi się zmieścić.

Olej to, bo to jakaś abstrakcja :D Jak się nie mieści w rejestrze, to oczywiście nikt do rejestru nie wstawia jego adresu, bo przecież po instrukcji return tego obiektu już nie będzie ;] Na cholerę komu taki adres? ;) C++ w przypadku funkcji Object function(int x, int y) zrobi tak, że za każdym razem, widząc w main wywołanie function(5,6) kolejno na stosie ląduje:

  1. puste miejsce na Object - to miejsce "należeć" będzie do wywołującego
  2. adres powrotu + argumenty (to się przyda funkcji wołanej)
  3. function tworzy swoje obiekty i z nimi pracuje
  4. instrukcja return nic na stos nie wstawia tylko kopiuje którąś zmienną lokalną do pustego miejsca (1)
  5. po skopiowaniu wszystko jest zdejmowane aż do miejsca (1)

Zakładając, że stos rośnie do góry, to będzie jakoś tak:

user image

Jak łatwo zauważyć, taka strategia jest trochę głupawa, bo przez cały czas działania funkcji function i tak na stosie znajduje się "dziura", którą można by było od razu wykorzystać, do w niej wsadzić jakąś zmienną lokalną, która należy do function. W szczególności do tej dziury można by wsadzić zmienną lokalną, która jest przez funkcję zwracana (kopiowania unikniemy). Aha, pytanie, czy tej dziury i kopiowania można się pozbyć, jeśli nie używasz wyniku? W ogólnym przypadku nie - bo jeśli function jest zdefiniowana w innym pliku, to ona nie wie, czy ktoś będzie z wyniku korzystał czy nie. Jej kod będzie tak wygenerowany, że zawsze będzie się tej dziury tam spodziewała tuż pod argumentami wstawionymi na stos i zawsze będzie tam kopiować podczas return. W szczególnym przypadku, kiedy kod wszystkiego jest znany, no to można oczywiście cuda robić.

2
Ranides napisał(a)

Zakładając, że stos rośnie do góry, to będzie jakoś tak:

Jest to wszystko, oczywiście, zależne od systemu i od kompilatora. Przykładowo w 64-bitowym Windows dochodzą tutaj 32 bajty na absurdalny “shadow space”, co oznacza że każde wywołanie funkcji marnuje 32 bajty na stosie (plus to co normalnie trzeba, np. adres powrotu) i ewentualnie 8 bajtów wyrównania, bo stos musi być wyrównany do granicy 16 bajtów.
Marnotrawstwo stosu straszne.

Wywołanie void foobar(void); pod Win32 to tylko 4 bajty stosu na adres powrotu, a pod Win64 aż 48 (32 bajty shadow space, 8 bajtów na powrót, 8 bajtów wyrównania).
Tu trzeba przyznać że te dodatkowe bajty funkcja może wykorzystać na co chce, więc różnica się zmniejszy gdy dojdą zmienne lokalne (Win32 alokuje dalsze miejsce, Win64 teoretycznie może wykorzystać shadow space i alignment).

0

Dzięki Panowie! :)

  1. Ok, z tego wynika, że można ZAWSZE swobodnie zwracać wartość? Czy w takim razie, zwracanie referencji ma sens tylko wtedy, gdy chcemy działać na określonym obiekcie/kontenerze? Bo jak widać RVO załatwia za nas sprawę.

  2. Zauważyłem, że przy wyłączonej optymalizacji gdy jest zdefiniowany move konstruktor to on jest wykorzystywany. Nie rozumiem jednak, w jaki sposób ma nam pomóc tu MoveConstructor ( poprawnie napisany).
    Foo func(){
    Foo obj;
    return obj;
    }
    Noi co on nam pomoże? Tzn. w czym będzie lepszy od zwykłego konstruktora kopiującego? W ogóle wydawało mi się, że Move konstruktor służy do przenoszenia obiektów tymaczasowych, a przecież obj taki nie jest.

1

Czy w takim razie, zwracanie referencji ma sens tylko wtedy, gdy chcemy działać na określonym obiekcie/kontenerze?
Na pewno nie wolno zwracać referencji na zmienną lokalną w funkcji, bo zmienna ta jest niszczona gdy kończy się funkcja.

Foo& func()
{
    Foo obj;
    return obj; // aaaaargh!!!
}

Foo func()
{
    Foo obj;
    return obj; // ok
}

Nie rozumiem jednak, w jaki sposób ma nam pomóc tu MoveConstructor ( poprawnie napisany). [...] Tzn. w czym będzie lepszy od zwykłego konstruktora kopiującego? W ogóle wydawało mi się, że Move konstruktor służy do przenoszenia obiektów tymaczasowych, a przecież obj taki nie jest.
Popatrz na zewnątrz wywołania funkcji:

Foo bar = func();

Generalnie powinno tu zadziałać RVO i utworzyć wartość zwracaną bezpośrednio w zmiennej bar. Ale RVO nie zawsze da się zastosować, ponadto jest tylko optymalizacją, zależną od parametrów kompilacji.
Więc jeśli nie ma RVO, to klasyczny kompilator tworzy obiekt tymczasowy (tym obiektem tymczasowym jest jakby func()), kopiuje go do bar, i destruuje func(). mamy więc niepotrzebną kopie.
Kompilator C++11 może zamiast konstruktora kopiującego wykorzystać kostruktor przesuwający, który zakłada możliwość uszkodzenia obiektu źródłowego, po czym również wywołany jest destruktor na obiekcie func().

1
Azarien napisał(a):
Foo func()
{
    Foo obj;
    return obj; // ok
}

Dla RVO najlepiej byłoby gdyby to wyglądało tak:

Foo func()
{
    return Foo(...);
}

Przy zwracaniu obiektu mogą powstać od 1 do dwóch tymczasowych kopii. W tym pierwszym wypadku mogą być nawet dwie, w drugim - jedna lub zero.

0

Jeśli chodzi o zwracanie obiektu np:

CObiekt GetObiekt()
{
CObiekt obiekt();
obiekt.var = 0;
return obiekt;
}
..
main(..)
{
CObiekt obiekt = GetObiekt();
}

to z tego co pamiętam kompilator(msvc) robi to tak:

CObiekt * GetObiekt(CObiekt * ptr)
{
ptr->var = 0;
return ptr;
}
..
main(..)
{
CObiekt obiekt;
GetObiekt(&obiekt);
}

Mogę się mylić ale tak mi się wydawało.

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