Move Semantics & std::move
Κατανοήστε το πρόβλημα που λύνει η σημασιολογία μετακίνησης, πώς το std::move είναι ένα cast (όχι μια κίνηση), εφαρμόστε τους κατασκευαστές κίνησης και την ανάθεση μετακίνησης και μάθετε γιατί το noexcept είναι κρίσιμο για την απόδοση του κοντέινερ.
Η κίνηση του προβλήματος λύνεται
Πριν από το C++11, η επιστροφή ενός μεγάλου αντικειμένου από μια συνάρτηση απαιτούσε αντιγραφή του. Η αντιγραφή ενόςstd::vectorμε 1 εκατομμύριο στοιχεία σημαίνει εκχώρηση νέας μνήμης, αντιγραφή κάθε στοιχείου — O(n) έργο που χάνεται εντελώς εάν η πηγή πρόκειται να καταστραφεί.
Μετακίνηση σημασιολογίας λύστε αυτό: αντί να αντιγράψουμε, κλέβουμε τους εσωτερικούς πόρους της πηγής (τον δείκτη, το μέγεθος, τη χωρητικότητά της) και αφήνουμε την πηγή σε έγκυρη αλλά κενή κατάσταση. Η μετακίνηση ενός διανύσματος είναι O(1) — απλώς αλλάξτε τρεις δείκτες/ακέραιους αριθμούς. Η πηγή δεν καταστρέφεται. γίνεται ένα κενό διάνυσμα.
Η βασική ιδέα: εάν ένα αντικείμενο είναι προσωρινό ή έχει επισημανθεί ως "Δεν το χρειάζομαι πια" (μέσωstd::move), είναι ασφαλές να κλέψετε τους πόρους του.
std::move Είναι Cast, Όχι Μετακίνηση
Τοstd::move(x)δεν μετακινεί τίποτα. Είναι ένα cast χωρίς όρους σε μια αναφορά rvalue (static_cast). Λέει στον μεταγλωττιστή: "Τελείωσα με αυτό το αντικείμενο — μπορείτε να το αντιμετωπίσετε ως προσωρινό."
Η πραγματική κίνηση συμβαίνει όταν καλείται ένας move constructor ή move assignment operator με την προκύπτουσα αναφορά rvalue. Εάν ο τύπος δεν έχει λειτουργίες μετακίνησης (π.χ. αντικείμεναconst), τοstd::moveεπιστρέφει σιωπηλά σε ένα αντίγραφο.
Αυτή είναι μια κοινή πηγή σφαλμάτων:const std::string s = "hello"; auto s2 = std::move(s);αντίγραφαsεπειδή τοstd::move(s)παράγειconst std::string&&, το οποίο συνδέεται με την παράμετροconst std::string&του κατασκευαστή αντιγραφής.
Εφαρμογή Move Constructor & Move Assignment
Ένας κατασκευαστής κίνησης κλέβει πόρους από την πηγή. Ένας χειριστής ανάθεσης μετακίνησης απελευθερώνει τους δικούς του πόρους και στη συνέχεια κλέβει από την πηγή. Και οι δύο πρέπει να αφήσουν την πηγή σε έγκυρη, καταστρεπτή κατάσταση.
#include <iostream>
#include <cstring>
#include <utility>
#include <algorithm>
class Buffer {
char* data_;
std::size_t size_;
public:
// Constructor
explicit Buffer(std::size_t n) : data_(new char[n]()), size_(n) {
std::cout << "Alloc " << n << " bytes\n";
}
// Destructor
~Buffer() {
delete[] data_;
std::cout << "Free (" << size_ << ")\n";
}
// Copy constructor (expensive — O(n))
Buffer(const Buffer& other) : data_(new char[other.size_]), size_(other.size_) {
std::memcpy(data_, other.data_, size_);
std::cout << "Copy " << size_ << " bytes\n";
}
// Move constructor (cheap — O(1)) — MUST be noexcept
Buffer(Buffer&& other) noexcept
: data_(other.data_), size_(other.size_) {
other.data_ = nullptr; // leave source in valid state
other.size_ = 0;
std::cout << "Move " << size_ << " bytes\n";
}
// Move assignment — MUST be noexcept
Buffer& operator=(Buffer&& other) noexcept {
if (this != &other) {
delete[] data_; // release own resources
data_ = other.data_; // steal source's resources
size_ = other.size_;
other.data_ = nullptr; // leave source valid
other.size_ = 0;
}
return *this;
}
// Copy assignment (omitted for brevity)
std::size_t size() const { return size_; }
};
int main() {
Buffer a(1'000'000); // Alloc 1000000 bytes
Buffer b = std::move(a); // Move 1000000 bytes — O(1)!
std::cout << "a.size: " << a.size() << '\n'; // 0 (moved-from)
std::cout << "b.size: " << b.size() << '\n'; // 1000000
}Τοstd::vectorθα μετακινήσει στοιχεία κατά την ανακατανομή μόνο εάν ο κατασκευαστής μετακίνησης είναιnoexcept. Εάν ο κατασκευαστής μετακίνησης μπορεί να ρίξει, το διάνυσμα πρέπει να αντιγράψει αντ' αυτού — διαφορετικά, εάν μια κίνηση φτάσει στα μισά της ανακατανομής, το διάνυσμα θα ήταν σε ασυνεπή κατάσταση με ορισμένα στοιχεία να μετακινηθούν και ορισμένα όχι.
Αυτό σημαίνει ότι η παράλειψη τουnoexceptστον κατασκευαστή μετακίνησης υποβαθμίζει σιωπηλά την απόδοση του διανύσματος από O(1) ανά στοιχείο σε O(n). Αυτό είναι ένα από τα πιο κοινά σφάλματα απόδοσης στο C++.
// BAD — vector will COPY during reallocation
Widget(Widget&& other) { ... }
// GOOD — vector will MOVE during reallocation
Widget(Widget&& other) noexcept { ... }Always use static_assert(std::is_nothrow_move_constructible_v για επαλήθευση.
Το Μετακομισμένο από την Πολιτεία
Μετά από μια κίνηση, το αντικείμενο προέλευσης βρίσκεται σε έγκυρη αλλά απροσδιόριστη κατάσταση. Το πρότυπο εγγυάται:
- Το αντικείμενο μπορεί να καταστραφεί με ασφάλεια.
- Το αντικείμενο μπορεί να ανατεθεί σε (δίνοντάς του μια νέα τιμή).
- Για τυπικούς τύπους βιβλιοθήκης, όλες οι λειτουργίες χωρίς προϋποθέσεις λειτουργούν (π.χ.size(),empty(),clear()).
Το πρότυπο ΔΕΝ εγγυάται ότι το αντικείμενο που έχει μετακινηθεί από είναι κενό. Στην πράξη, ταstd::stringκαιstd::vectorείναι συνήθως άδεια μετά από μια κίνηση, αλλά αυτό είναι μια λεπτομέρεια υλοποίησης.
Κανόνας παραγωγής: μετά τη μετακίνηση από ένα αντικείμενο, μην διαβάζετε την τιμή του — μόνο καταστρέψτε το ή εκχωρήστε μια νέα τιμή.
Όταν ο μεταγλωττιστής δημιουργεί αυτόματα κινήσεις
Ο μεταγλωττιστής δημιουργεί λειτουργίες μετακίνησης μόνο εάν η κλάση δεν έχει λειτουργίες αντιγραφής δηλωμένες από τον χρήστη, λειτουργίες μετακίνησης δηλωμένες από τον χρήστη και καταστροφέα δηλωμένο από τον χρήστη (Κανόνας Πέντε/Μηδέν). Εάν δηλώσετε ΟΠΟΙΟΔΗΠΟΤΕ από αυτά, θα πρέπει να τα δηλώσετε ΟΛΑ.
#include <string>
#include <vector>
#include <iostream>
// GOOD: Rule of Zero — compiler generates everything
struct Employee {
std::string name;
std::vector<std::string> skills;
int id;
// No user-declared special members — compiler generates all 5
};
// BAD: Rule of Five violation — destructor suppresses moves
struct Resource {
int* data;
Resource() : data(new int(42)) {}
~Resource() { delete data; } // move operations NOT generated!
// This class will COPY where it could move
// Fix: declare all five special members
};
int main() {
Employee e1{"Alice", {"C++", "Rust"}, 1};
Employee e2 = std::move(e1); // compiler-generated move
std::cout << e2.name << '\n'; // "Alice"
static_assert(std::is_nothrow_move_constructible_v<Employee>,
"Employee should be nothrow-movable");
// static_assert(std::is_nothrow_move_constructible_v<Resource>,
// "FAILS — Resource has no move constructor");
}1. Ακολουθήστε τον κανόνα του μηδέν — αφήστε τον μεταγλωττιστή να δημιουργήσει ειδικά μέλη χρησιμοποιώντας τύπους RAII (string,vector,unique_ptr) ως μέλη.
2. Να επισημαίνετε πάντα τις λειτουργίες μετακίνησηςnoexcept — διαφορετικά τοstd::vectorδεν θα τις χρησιμοποιήσει.
3. Τοstd::moveσε ένα αντικείμενοconstαντιγράφει αθόρυβα — παρακολουθήστε αυτό στις αναθεωρήσεις κώδικα.
4. Μην μετακινηθείτε από αντικείμενα που χρειάζεστε ακόμα — χρησιμοποιήστε τοstd::moveμόνο στην τελευταία χρήση.
5. Μην επιστρέψετε τοstd::move(local) — έτσι αποτρέπεται η διαγραφή αντιγραφής (NRVO). Απλώς επιστρέψτε το τοπικό με το όνομα.
6. Επαλήθευση μεstatic_assert(is_nothrow_move_constructible_v.
- Moving steals resources (O(1)) instead of copying them (O(n))
std::moveis just a cast to T&& — the actual move happens in the move constructor/assignment- Move constructors and move assignment must be
noexceptfor vector to use them - Moved-from objects are in a valid but unspecified state — only destroy or reassign
- The compiler auto-generates moves only if no copy/move/destructor is user-declared (Rule of Zero)
Κουίζ — Δοκιμάστε τις γνώσεις σας
(20 XP)1. What does`std::move(x)`actually do?
2. Why must move constructors be marked`noexcept`?
3. What happens with`const std::string s = "hi"; auto s2 = std::move(s);`?