Klasa operator przypisania kopiowania (operator=) służy do kopiowania wartości z jednego obiektu do innego już istniejącego obiektu.
Powiązana treść
Począwszy od C++11, C++ obsługuje także opcję „Przenieś przypisanie”. Przypisywanie przenoszenia omawiamy na lekcji 22.3 -- Przenieś konstruktory i przenieś przypisanie.
Przypisanie kopiowania a konstruktor kopiowania
Zadanie konstruktora kopiującego i operatora przypisania kopiowania jest prawie równoważne — oba kopiują jeden obiekt do drugiego. Konstruktor kopiujący inicjuje jednak nowe obiekty, natomiast operator przypisania zastępuje zawartość istniejących obiektów.
Różnica pomiędzy konstruktorem kopiującym a operatorem przypisania kopiującego powoduje sporo zamieszania wśród nowych programistów, ale tak naprawdę nie jest to wcale takie trudne. Podsumowując:
- Jeśli nowy obiekt musi zostać utworzony, zanim nastąpi kopiowanie, używany jest konstruktor kopiujący (uwaga: obejmuje to przekazywanie lub zwracanie obiektów przez wartość).
- Jeśli nie ma potrzeby tworzenia nowego obiektu, zanim nastąpi kopiowanie, używany jest operator przypisania.
Przeciążanie operatora przypisania
Przeciążanie operatora przypisania kopiowania (operator=) jest dość proste, z jednym konkretnym zastrzeżeniem, do którego dojdziemy. Operator przypisania kopiowania musi być przeciążony jako funkcja składowa.
#include <cassert>
#include <iostream>
class Fraction
{
private:
int m_numerator { 0 };
int m_denominator { 1 };
public:
// Default constructor
Fraction(int numerator = 0, int denominator = 1 )
: m_numerator { numerator }, m_denominator { denominator }
{
assert(denominator != 0);
}
// Kopiuj konstruktor
Fraction(const Fraction& copy)
: m_numerator { copy.m_numerator }, m_denominator { copy.m_denominator }
{
// nie ma potrzeby sprawdzania tutaj mianownika 0, ponieważ kopia musi już być prawidłowym ułamkiem
std::cout << "Copy constructor called\n"; // tylko żeby udowodnić, że to działa
}
// Overloaded assignment
Fraction& operator= (const Fraction& fraction);
friend std::ostream& operator<<(std::ostream& out, const Fraction& f1);
};
std::ostream& operator<<(std::ostream& out, const Fraction& f1)
{
out << f1.m_numerator << '/' << f1.m_denominator;
return out;
}
// Uproszczona implementacja operatora= (patrz lepsza implementacja poniżej)
Fraction& Fraction::operator= (const Fraction& fraction)
{
// zrób kopię
m_numerator = fraction.m_numerator;
m_denominator = fraction.m_denominator;
// zwróć istniejący obiekt, abyśmy mogli połączyć ten operator
return *this;
}
int main()
{
Fraction fiveThirds { 5, 3 };
Fraction f;
f = fiveThirds; // wywołania przeciążonego przypisania
std::cout << f;
return 0;
}Wypisuje:
5/3
To wszystko powinno być już całkiem proste. Nasz przeciążony operator= zwraca *to, abyśmy mogli połączyć ze sobą wiele przypisań:
int main()
{
Fraction f1 { 5, 3 };
Fraction f2 { 7, 2 };
Fraction f3 { 9, 5 };
f1 = f2 = f3; // chained assignment
return 0;
}Problemy wynikające z samodzielnego przypisania
W tym miejscu sprawy zaczynają się robić nieco bardziej interesujące. C++ umożliwia samodzielne przypisanie:
int main()
{
Fraction f1 { 5, 3 };
f1 = f1; // self assignment
return 0;
}Wywoła to f1.operator=(f1) i zgodnie z uproszczoną implementacją powyżej wszyscy członkowie zostaną przypisani do siebie. W tym konkretnym przykładzie samoprzypisanie powoduje, że każdy członek zostaje przypisany do siebie, co nie ma żadnego ogólnego wpływu poza stratą czasu. W większości przypadków samodzielne przypisanie nie musi w ogóle nic robić!
Jednak w przypadkach, gdy operator przypisania musi dynamicznie przypisywać pamięć, samodzielne przypisanie może być w rzeczywistości niebezpieczne:
#include <algorithm> // dla std::max i std::copy_n
#include <iostream>
class MyString
{
private:
char* m_data {};
int m_length {};
public:
MyString(const char* data = nullptr, int length = 0 )
: m_length { std::max(length, 0) }
{
if (length)
{
m_data = new char[static_cast<std::size_t>(length)];
std::copy_n(data, length, m_data); // skopiuj elementy długości danych do m_data
}
}
~MyString()
{
delete[] m_data;
}
MyString(const MyString&) = default; // niektóre kompilatory (gcc) ostrzegają, jeśli masz elementy wskaźnikowe, ale nie zadeklarowano konstruktora kopiującego
// Overloaded assignment
MyString& operator= (const MyString& str);
friend std::ostream& operator<<(std::ostream& out, const MyString& s);
};
std::ostream& operator<<(std::ostream& out, const MyString& s)
{
out << s.m_data;
return out;
}
// Prosta implementacja operatora= (nie używać)
MyString& MyString::operator= (const MyString& str)
{
// jeśli w bieżącym ciągu istnieją dane, usuń je
if (m_data) delete[] m_data;
m_length = str.m_length;
m_data = nullptr;
// przydziel nową tablicę o odpowiedniej długości
if (m_length)
m_data = new char[static_cast<std::size_t>(str.m_length)];
std::copy_n(str.m_data, m_length, m_data); // kopiuje m_length elementy str.m_data do m_data
// zwróć istniejący obiekt, abyśmy mogli połączyć ten operator
return *this;
}
int main()
{
MyString alex("Alex", 5); // Meet Alex
MyString employee;
employee = alex; // Alex jest naszym obecnym pracownikiem
std::cout << employee; // Powiedz swoje imię, pracowniku
return 0;
}Najpierw uruchom program w niezmienionej postaci. Zobaczysz, że program wypisuje „Alex” tak, jak powinien.
Teraz uruchom następujący program:
int main()
{
MyString alex { "Alex", 5 }; // Meet Alex
alex = alex; // Alex jest sobą
std::cout << alex; // Powiedz swoje imię, Alex
return 0;
}Prawdopodobnie otrzymasz wynik śmieciowy. Co się stało?
Zastanów się, co dzieje się w przeciążonym operator=, gdy zarówno ukryty obiekt ORAZ przekazany parametr (str) są zmiennymi alex. W tym przypadku m_data jest tym samym, co str.m_data. Pierwszą rzeczą, która się dzieje, jest to, że funkcja sprawdza, czy ukryty obiekt ma już ciąg znaków. Jeśli tak, musi go usunąć, aby nie doszło do wycieku pamięci. W tym przypadku przydzielane jest m_data, więc funkcja usuwa m_data. Ale ponieważ str jest tym samym co *this, ciąg, który chcieliśmy skopiować, został usunięty, a m_data (i str.m_data) pozostają w zawieszeniu.
Później przydzielamy nową pamięć do m_data (i str.m_data). Zatem kiedy później kopiujemy dane z str.m_data do m_data, kopiujemy śmieci, ponieważ str.m_data nigdy nie zostało zainicjowane.
Wykrywanie i obsługa samodzielnego przypisania
Na szczęście możemy wykryć, kiedy następuje samoprzypisanie. Oto zaktualizowana implementacja naszego przeciążonego operatora= dla klasy MyString:
MyString& MyString::operator= (const MyString& str)
{
// sprawdzenie samodzielnego przypisania
if (this == &str)
return *this;
// jeśli w bieżącym ciągu istnieją dane, usuń je
if (m_data) delete[] m_data;
m_length = str.m_length;
m_data = nullptr;
// przydziel nową tablicę o odpowiedniej długości
if (m_length)
m_data = new char[static_cast<std::size_t>(str.m_length)];
std::copy_n(str.m_data, m_length, m_data); // kopiuje m_length elementy str.m_data do m_data
// zwróć istniejący obiekt, abyśmy mogli połączyć ten operator
return *this;
}Dzięki sprawdzeniu, czy adres naszego ukrytego obiektu jest taki sam, jak adres obiektu przekazywanego jako parametr, możemy sprawić, że nasz operator przypisania po prostu zwróci natychmiast, nie wykonując żadnej innej pracy.
Ponieważ jest to tylko porównanie wskaźników, powinno być szybkie i nie wymaga przeciążenia operatora==.
Jeśli nie do obsługi samodzielnego przypisania
Zazwyczaj sprawdzanie samodzielnego przypisania jest pomijane w przypadku konstruktorów kopiujących. Ponieważ kopiowany obiekt jest nowo utworzony, jedynym przypadkiem, w którym nowo utworzony obiekt może być równy kopiowanemu obiektowi, jest próba zainicjowania nowo zdefiniowanego obiektu samym sobą:
someClass c { c };W takich przypadkach kompilator powinien Cię ostrzec, że c jest niezainicjowaną zmienną.
Po drugie, sprawdzenie samodzielnego przydziału można pominąć w klasach, które w naturalny sposób radzą sobie z samodzielnym przydziałem. Rozważmy ten operator przypisania klasy Ułamek, który ma zabezpieczenie przed samodzielnym przypisaniem:
// Lepsza implementacja operatora=
Fraction& Fraction::operator= (const Fraction& fraction)
{
// ochroniarz z własnym przydziałem
if (this == &fraction)
return *this;
// zrób kopię
m_numerator = fraction.m_numerator; // potrafi poradzić sobie z samodzielnym przydziałem
m_denominator = fraction.m_denominator; // potrafi poradzić sobie z samodzielnym przydziałem
// zwróć istniejący obiekt, abyśmy mogli połączyć ten operator
return *this;
}Gdyby zabezpieczenie przed samoprzypisaniem nie istniało, ta funkcja nadal działałaby poprawnie podczas samodzielnego przydziału (ponieważ wszystkie operacje wykonywane przez tę funkcję mogą poprawnie obsłużyć samodzielne przypisanie).
Ponieważ samodzielne przypisanie jest rzadkim zdarzeniem, niektórzy wybitni guru C++ zalecają pomijanie tego zabezpieczenia nawet w klasach, które odniosłyby z tego korzyści. Nie zalecamy tego, ponieważ uważamy, że lepszą praktyką jest kodowanie defensywne, a później selektywna optymalizacja.
Idiom kopiowania i zamiany
Lepszym sposobem radzenia sobie z problemami związanymi z samodzielnym przypisaniem jest tak zwany idiom kopiowania i zamiany. Istnieje świetny opis działania tego idiomu w przypadku przepełnienia stosu.
niejawny operator przypisania kopiowania
W przeciwieństwie do innych operatorów, kompilator zapewni niejawny publiczny operator przypisania kopii dla twojej klasy, jeśli nie podasz operatora zdefiniowanego przez użytkownika. Ten operator przypisania dokonuje przypisania członkowskiego (co jest zasadniczo takie samo jak inicjalizacja członkowa wykonywana przez domyślne konstruktory kopiujące).
Podobnie jak inne konstruktory i operatory, możesz zapobiec wykonywaniu przypisań, ustawiając operator przypisania kopiowania jako prywatny lub używając słowa kluczowego usuwania:
#include <cassert>
#include <iostream>
class Fraction
{
private:
int m_numerator { 0 };
int m_denominator { 1 };
public:
// Default constructor
Fraction(int numerator = 0, int denominator = 1)
: m_numerator { numerator }, m_denominator { denominator }
{
assert(denominator != 0);
}
// Kopiuj konstruktor
Fraction(const Fraction ©) = delete;
// Overloaded assignment
Fraction& operator= (const Fraction& fraction) = delete; // brak odpowiedzi poprzez przypisanie!
friend std::ostream& operator<<(std::ostream& out, const Fraction& f1);
};
std::ostream& operator<<(std::ostream& out, const Fraction& f1)
{
out << f1.m_numerator << '/' << f1.m_denominator;
return out;
}
int main()
{
Fraction fiveThirds { 5, 3 };
Fraction f;
f = fiveThirds; // błąd kompilacji, operator= został usunięty
std::cout << f;
return 0;
}Zauważ, że jeśli twoja klasa ma stałe elementy członkowskie, kompilator zamiast tego zdefiniuje ukryty operator= jako usunięty. Dzieje się tak, ponieważ nie można przypisać elementów const, więc kompilator przyjmie, że Twojej klasy nie powinno można przypisywać.
Jeśli chcesz, aby klasa ze składnikami const była możliwa do przypisania (dla wszystkich członków, którzy nie są stałymi), będziesz musiał jawnie przeciążyć operator= i ręcznie przypisać każdy element inny niż const.

