• Najnowsze pytania
  • Bez odpowiedzi
  • Zadaj pytanie
  • Kategorie
  • Tagi
  • Zdobyte punkty
  • Ekipa ninja
  • IRC
  • FAQ
  • Regulamin
  • Książki warte uwagi

C++, konstruktory przenoszące i std::move ( kilka pytań )

0 głosów
73 wizyt
pytanie zadane 15 kwietnia w C i C++ przez Jakub 0 Stary wyjadacz (12,500 p.)

Witam, kończę książkę C++ Szkoła Programowania, jestem przy jednym z ostatnich zadań. Co prawda napisałem go bez większego problemu, jednak nasunęło mi się wiele pytań i kwestii które nie rozumiem.

Oto kod:
 

#include <iostream>
#include <string>

class Cpmv {
public:
	struct Info {
		std::string qcode;
		std::string zcode;
	};
private:
	Info* pi;
public:
	Cpmv();
	Cpmv(std::string q, std::string z);
	Cpmv(const Cpmv& cp);
	Cpmv(Cpmv&& mv);
	~Cpmv();
	Cpmv& operator=(const Cpmv& cp);
	Cpmv& operator=(Cpmv&& mv);
	Cpmv operator+(const Cpmv& obj) const;
	void Display() const;
};

///--------------------------------------------------------------------------

Cpmv::Cpmv() {
	std::cout << "start: Cpmv()\n";
	pi = new Info;
	pi->zcode = "";
	pi->qcode = "";
	std::cout << "koniec: Cpmv()\n";
}

Cpmv::Cpmv(std::string q, std::string z) {
	std::cout << "start: Cpmv(std::string q = \"" << q << "\", std::string z = \"" << z << "\")\n";
	pi = new Info;
	pi->qcode = q;
	pi->zcode = z;
	std::cout << "koniec: Cpmv(std::string q = \"" << q << "\", std::string z = \"" << z << "\")\n";
}

Cpmv::Cpmv(const Cpmv& cp) {
	std::cout << "start: Cpmv(const Cpmv& cp)\n";
	pi = new Info;
	*pi = *cp.pi;
	std::cout << "koniec: Cpmv(const Cpmv& cp)\n";
}

Cpmv::Cpmv(Cpmv&& mv) {
	std::cout << "start: Cpmv(Cpmv&& mv)\n";
	pi = std::move(mv.pi);
	std::cout << "koniec: Cpmv(Cpmv&& mv)\n";
}

Cpmv::~Cpmv() {
	std::cout << "start: ~Cpmv()\n";
	delete pi;
	std::cout << "koniec: ~Cpmv()\n";
}

Cpmv& Cpmv::operator=(const Cpmv& cp) {
	std::cout << "start: operator=(const Cpmv& cp)\n";
	if (this == &cp) {
		std::cout << "koniec ( \"nietypowy\" ): operator=(const Cpmv& cp)\n";
		return *this;
	}
	delete pi;
	pi = new Info(*cp.pi);
	std::cout << "koniec: operator=(const Cpmv& cp)\n";
	return *this; 
}

Cpmv& Cpmv::operator=(Cpmv&& mv) {
	std::cout <<"start: operator=(Cpmv&& cmv)\n";
	if (this == &mv) {
		std::cout << "koniec ( \"nietypowy\" ): operator=(Cpmv&& cmv)\n";
		return *this; 
	}
	pi = mv.pi; //--------------- (1)
	mv.pi = nullptr; 
	std::cout << "koniec: operator=(Cpmv&& cmv)\n";
	return *this;
}

Cpmv Cpmv::operator+(const Cpmv& obj) const {
	std::cout << "start: operator+(const Cpmv& obj) const\n";
	Cpmv temp(pi->qcode, pi->zcode);
	temp.pi->qcode += obj.pi->qcode;
	temp.pi->zcode += obj.pi->zcode;
	std::cout << "koniec: operator+(const Cpmv& obj) const\n";
	return Cpmv(temp); //------------------ (2)
}

void Cpmv::Display() const {
	std::cout << "start: Display() const\n";
	std::cout << "qcode: " << pi->qcode << std::endl;
	std::cout << "zcode: " << pi->zcode << std::endl;
	std::cout << "koniec: Display() const\n";
}

///--------------------------------------------------------------------------

void main2() {
	std::cout << "-------------------------:\n";
	Cpmv a;
	std::cout << "-------------------------:\n";
	Cpmv b("ala", "anna");
	std::cout << "-------------------------:\n";
	a = a + b; 
	std::cout << "-------------------------:\n";
	Cpmv c(a);
	std::cout << "-------------------------:\n";
	Cpmv d = a;
	std::cout << "-------------------------:\n";
	a.Display();
	std::cout << "-------------------------:\n";
	b.Display();
	std::cout << "-------------------------:\n";
	c.Display();
	std::cout << "-------------------------:\n";
	d.Display();
	std::cout << "-------------------------:\n";
	d = d + Cpmv(" dod1 ", " dod2 ");
	std::cout << "-------------------------:\n";
	d.Display(); 
	std::cout << "-------------------------:\n";
	Cpmv e(c + a);
	std::cout << "-------------------------:\n";
	e.Display(); 
	std::cout << "-------------------------:\n";
	c = std::move(c); // --------------- (3)
	std::cout << "-------------------------:\n";
	c.Display();
	std::cout << "-------------------------:\n";
}

Jest to nic innego jak prosta testowa klasa, zaznaczyłem numerkami rzeczy które nie do końca rozumiem w 100% :

(1) - Jeśli zamiast:

pi = mv.pi;
mv.pi = nullptr; 

napiszę:

pi = std::move(mv.pi);

to mam taki wyjątek:

ja rozumiem że przez to jest wyciek pamięci, ale co to ma wspólnego z długością łańcucha w obiekcie klasy std::string ?!

W ogólne nie wiem co w tym kontekście robi std::move(), również w konstruktorze przenoszącym:

Cpmv::Cpmv(Cpmv&& mv) {
	std::cout << "start: Cpmv(Cpmv&& mv)\n";
	pi = std::move(mv.pi);
	std::cout << "koniec: Cpmv(Cpmv&& mv)\n";
}

ponoć funkcja ta zamienia l-wartości na r-wartości, ale przecież mv to już r-wartość. Zastosowałem w tym konstruktorze std::move() dlatego że ogólnie widzę to w kodzie innych, również w dokumentacji: https://en.cppreference.com/w/cpp/utility/move

(2) - w tej metodzie:

Cpmv Cpmv::operator+(const Cpmv& obj) const {
	std::cout << "start: operator+(const Cpmv& obj) const\n";
	Cpmv temp(pi->qcode, pi->zcode);
	temp.pi->qcode += obj.pi->qcode;
	temp.pi->zcode += obj.pi->zcode;
	std::cout << "koniec: operator+(const Cpmv& obj) const\n";
	return Cpmv(temp); 
}

jeśli nie dam przy return nowego obiektu tymczasowego:

Cpmv(temp)

to program się wysypuje z powodu braku dostępu do pamięci, dlaczego? Przecież automatycznie powinna wykonać się kopia temp.

(3)

c = std::move(c);

tą instrukcję dałem dla testu, co tu się dokładnie dzieje?

ustawiłem nawet w operatorze przypisania dla r-wartości zabezpieczenie przed tym:


Cpmv& Cpmv::operator=(Cpmv&& mv) {
	std::cout <<"start: operator=(Cpmv&& cmv)\n";
	if (this == &mv) {
		std::cout << "koniec ( \"nietypowy\" ): operator=(Cpmv&& cmv)\n";
		return *this; 
	}
	pi = mv.pi; 
	mv.pi = nullptr; 
	std::cout << "koniec: operator=(Cpmv&& cmv)\n";
	return *this;
}

czy jest to uzasadnione?

Z góry dziękuje za pomoc, mam nadzieje że rozumiecie moje pytania... jak coś to pytajcie :)

komentarz 15 kwietnia przez Hiskiel Pasjonat (22,580 p.)
//offtop

Nie wiedziałem, że MS stworzył C++..

1 odpowiedź

+1 głos
odpowiedź 15 kwietnia przez adrian17 Mędrzec (178,200 p.)
wybrane 15 kwietnia przez Jakub 0
 
Najlepsza

To ja może tylko na pierwszym się skupię:

pi = mv.pi;
mv.pi = nullptr;

To ustawia pi oraz ustawia w poprzednim obiekcie wskaźnik na null - czyli efektywnie "przenosi" właściciela wskaźnika na nowy obiekt.

pi = std::move(mv.pi);

To... w zasadzie nic nie robi ciekawego. `pi` jest wskaźnikiem, to wynik końcowy tej linii niczym nie różni się od `pi = mv.pi`. To oznacza że poprzedni obiekt wciąż ma wskaźnik na ten obiekt, więc wywoła na nim `delete`, więc nowy obiekt będzie potencjalnie miał martwy wskaźnik.

Przy czym, dokładnie ten błąd robisz w `Cpmv::Cpmv(Cpmv&& mv)`. (Możliwe, że naprawienie tego naprawi też inne punkty? Ale temu się nie przyglądałem)

komentarz 15 kwietnia przez Jakub 0 Stary wyjadacz (12,500 p.)

Tak pomyślałem że samo:

pi = mv.pi;
mv.pi = nullptr;

w operatorze przypisania to za mało, no bo jeśli 'pi' wskazywało na coś wcześniej to dostęp do tej pamięci zostanie utracony ( wyciek ). Więc powinienem wcześniej zwolnić pamięć na którą wskazywało 'pi' lub wykorzystać jakąś zmienną pomocniczą do zamiany wskaźników miejscami ( lub funkcje std::swap ). Kolejny problem polega na tym że chyba już sam nie rozumiem jak działa std::move(), np w tej sytuacji:

pi = std::move(mv.pi);

myślałem że głównym problemem jest utrata dostępu do danych wskazywanych przez wcześniejsze 'pi' i wyciek z tej strony. A tu się okazuje że problem tkwi w mv.pi, myślałem że std::move powoduje przeniesienie , tzn. mv.pi automatycznie straci swoje dane.

komentarz 15 kwietnia przez adrian17 Mędrzec (178,200 p.)

w operatorze przypisania to za mało, no bo jeśli 'pi' wskazywało na coś wcześniej to dostęp do tej pamięci zostanie utracony ( wyciek ).

Chwila moment, ale przecież w operatorze przypisania obiekt dostaje wartość wskaźnika z drugiego obiektu - gdzie tutaj coś jest tracone?

komentarz 15 kwietnia przez Jakub 0 Stary wyjadacz (12,500 p.)
no ale jego wcześniejsza wartość zostanie nadpisana ( a tą wartością jest adres "bloku pamięci" przydzielonego operatorem new którą pasuje zwolnić )
komentarz 15 kwietnia przez adrian17 Mędrzec (178,200 p.)
No i? Wartość nie jest gubiona, bo teraz pierwszy obiekt (ten, do którego przenieśliśmy) ma dokładnie ten wskaźnik.
komentarz 15 kwietnia przez adrian17 Mędrzec (178,200 p.)
Wait... ah, teraz zrozumiałem, przepraszam. Myślałem że mówisz o drugim obiekcie. Tak, w operatorze przypisania wypada opcjonalnie zrobić `delete` na pierwszym obiekcie przed nadpisaniem `pi`.
komentarz 15 kwietnia przez Jakub 0 Stary wyjadacz (12,500 p.)

Zmodyfikowałem to tak:

Cpmv& Cpmv::operator=(Cpmv&& mv) {
	std::cout <<"start: operator=(Cpmv&& cmv)\n";
	if (this == &mv) {
		std::cout << "koniec ( \"nietypowy\" ): operator=(Cpmv&& cmv)\n";
		return *this; 
	}
	std::swap(pi, mv.pi);
	std::cout << "koniec: operator=(Cpmv&& cmv)\n";
	return *this;
}

i tu jak najbardziej się zgadza. Ale tak jak było wcześniej:

Cpmv& Cpmv::operator=(Cpmv&& mv) {
	std::cout <<"start: operator=(Cpmv&& cmv)\n";
	if (this == &mv) {
		std::cout << "koniec ( \"nietypowy\" ): operator=(Cpmv&& cmv)\n";
		return *this; 
	}
	pi = mv.pi; 
	mv.pi = nullptr; 
	std::cout << "koniec: operator=(Cpmv&& cmv)\n";
	return *this;
}

jest wadliwe, ponieważ:

musiał bym dać albo delete pi przed pi = mv.pi albo wykorzystać to swap.

komentarz 15 kwietnia przez adrian17 Mędrzec (178,200 p.)

musiał bym dać albo delete pi przed pi = mv.pi albo wykorzystać to swap.

Tak, trzeba to zrobić.

(sposób z std::swap też pewnie by spełnił zadanie i w sumie jest fajną mikrooptymalizacją, ale `delete` jest czytelniejsze)

komentarz 15 kwietnia przez Jakub 0 Stary wyjadacz (12,500 p.)
Mam jeszcze dość nietypowe pytanie, jak coś to nie musisz na nie absolutnie odpowiadać ;)

Więc ostatnio założyłem taki temat: https://forum.pasja-informatyki.pl/425353/qt-program-szyfrujacy-problem-z-xorowaniem-plikow-png-i-prawdopodobnie-innych-tez#c425519

Tam mi pomogłeś z kilkoma rzeczami, temat jednak się urwał. Piszę o tym bo nie wiem czy czekać czy może zaktualizować pytanie, zadać nowe... nie wiem czy w ogóle wypada zadać dwa razy to samo pytanie lub uaktualnić swoją odpowiedzią czy może lepiej skorzystać z innego forum. Bo mi bardzo zależy :)

* Przepraszam jeśli to wygląda jak bym się naprzykrzał, po prostu nie jestem pewny co mam zrobić.
komentarz 15 kwietnia przez adrian17 Mędrzec (178,200 p.)
(Nie miałem okazji odpisać, ale widziałem kod i domyślam się że przyczyna jest prosta - nie powinieneś przechodzić przez stringi/pola tekstowe do operacji na plikach binarnych; co najwyżej do prezentacji)
komentarz 15 kwietnia przez Jakub 0 Stary wyjadacz (12,500 p.)
No właśnie tak nie robię, nawet dla pola tekstowego ustawiłem readOnly(true).

Podobne pytania

+2 głosów
2 odpowiedzi 176 wizyt
pytanie zadane 14 czerwca 2016 w C i C++ przez ElPiwo Początkujący (370 p.)
0 głosów
3 odpowiedzi 304 wizyt
0 głosów
2 odpowiedzi 185 wizyt
pytanie zadane 5 września 2015 w C i C++ przez mrcnsct Nałogowiec (36,810 p.)
Porady nie od parady
Pytania na temat serwisu SPOJ należy zadawać z odpowiednią kategorią dotyczącą tej strony.SPOJ

63,241 zapytań

109,485 odpowiedzi

228,714 komentarzy

43,296 pasjonatów

Przeglądających: 337
Pasjonatów: 14 Gości: 323

Motyw:

Akcja Pajacyk

Pajacyk od wielu lat dożywia dzieci. Pomóż klikając w zielony brzuszek na stronie. Dziękujemy! ♡

Oto dwie polecane książki warte uwagi. Pełną listę znajdziesz tutaj.

...