Semantica di spostamento e rvalue
Fa parte della sezione Programmazione orientata agli oggetti del percorso C++ di Coddy. Lezione 83 di 104.
In C++, ogni espressione è un lvalue (ha un’identità persistente, può essere indirizzata) oppure un rvalue (temporanea, in procinto di essere distrutta). Comprendere questa distinzione permette di sfruttare la semantica di spostamento: una potente ottimizzazione che evita copie non necessarie.
Un riferimento rvalue, dichiarato con &&, si lega specificamente agli oggetti temporanei. Questo ti permette di "sottrarre" risorse agli oggetti che stanno per scomparire comunque:
#include <iostream>
#include <utility>
class Buffer {
int* data;
size_t size;
public:
Buffer(size_t s) : data(new int[s]), size(s) {
std::cout << "Constructed\n";
}
// Constructor de movimiento - se apropia de los recursos
Buffer(Buffer&& other) noexcept
: data(other.data), size(other.size) {
other.data = nullptr; // Deja el objeto de origen en un estado válido
other.size = 0;
std::cout << "Moved\n";
}
~Buffer() { delete[] data; }
};
int main() {
Buffer b1(1000);
Buffer b2(std::move(b1)); // Activa el constructor de movimiento
}La funzione std::move in realtà non sposta nulla: converte semplicemente un lvalue in un riferimento rvalue, segnalando che sei disposto a cedere le risorse dell’oggetto. Lo spostamento effettivo avviene nel costruttore di spostamento o nell’operatore di assegnazione per spostamento.
La semantica di spostamento migliora notevolmente le prestazioni quando si lavora con oggetti che richiedono molte risorse, come i contenitori o le stringhe. Invece di copiare in profondità megabyte di dati, trasferisci semplicemente la proprietà dei puntatori: un’operazione a tempo costante, indipendentemente dalle dimensioni.
Sfida
FacileCostruiamo una classe DataBuffer che gestisce le risorse e dimostra la semantica di spostamento in azione. Vedrai come spostare le risorse invece di copiarle può migliorare notevolmente l'efficienza quando si trasferisce la proprietà della memoria allocata dinamicamente.
Organizzerai il codice in tre file:
DataBuffer.h: definisci la classeDataBuffer, che gestisce un array di interi allocato dinamicamente.La tua classe dovrebbe avere membri privati per il puntatore ai dati (
int*), la dimensione (size_t) e un nome (std::string) per aiutarti a distinguere i buffer durante le operazioni.Dichiara quanto segue:
- Un costruttore che accetta un nome
std::stringe una dimensionesize_t, alloca l'array e stampa:[name] constructed with size [size] - Un costruttore di spostamento che accetta un riferimento rvalue, acquisisce le risorse e stampa:
[name] moved from [source_name](dove il buffer di destinazione acquisisce il nome della sorgente) - Un distruttore che stampa
[name] destroyed(oppureempty destroyedse il buffer è stato spostato) - Un metodo
getSize()che restituisce la dimensione corrente - Un metodo
getName()che restituisce il nome del buffer
Ricorda di contrassegnare il costruttore di spostamento come
noexcepte di lasciare l'oggetto sorgente in uno stato vuoto ma valido (nullptr, dimensione 0, nome "empty").- Un costruttore che accetta un nome
DataBuffer.cpp: implementa tutti i metodi dichiarati nell'header. Quando viene eseguito il distruttore, elimina i dati solo se il puntatore non è null. Includi<iostream>per l'output.main.cpp: leggi due valori in input:- Un nome per il buffer (stringa)
- Una dimensione per il buffer (intero)
Crea un
DataBuffercon il nome e la dimensione specificati. Poi crea un secondo buffer spostando il primo constd::move(). Dopo lo spostamento, stampa lo stato di entrambi i buffer:Original: [name] size=[size]New: [name] size=[size]
Includi
<utility>perstd::move.
Per esempio, con gli input Alpha e 100:
Alpha constructed with size 100
Alpha moved from Alpha
Original: empty size=0
New: Alpha size=100
Alpha destroyed
empty destroyedCon gli input Buffer e 50:
Buffer constructed with size 50
Buffer moved from Buffer
Original: empty size=0
New: Buffer size=50
Buffer destroyed
empty destroyedNota come il costruttore di spostamento trasferisca la proprietà della memoria allocata senza copiare alcun dato. Il buffer originale rimane in uno stato vuoto ma valido e, quando entrambi i buffer vengono distrutti alla fine del programma, solo quello che possiede ancora la memoria la elimina effettivamente.
Provalo tu
#include <iostream>
#include <string>
#include <utility>
#include "DataBuffer.h"
int main() {
std::string name;
int size;
std::cin >> name;
std::cin >> size;
// TODO: Crea un DataBuffer con el nombre y el tamaño dados
// TODO: Crea un segundo búfer moviendo el primero con std::move()
// TODO: Imprime el estado de ambos búferes:
// Original: [name] size=[size]
// Nuevo: [name] size=[size]
return 0;
}
Questa lezione include un breve quiz. Inizia la lezione per rispondere e tenere traccia dei tuoi progressi.
Tutte le lezioni di Programmazione orientata agli oggetti
1Fondamenti della programmazione orientata agli oggetti
File esterniBuild e compilazione in C++File header e file sorgenteNamespace e ambitoIntroduzione alla programmazione orientata agli oggetti in C++Classi e oggetti a confrontoIl puntatore 'this'Metodi (funzioni membro)Attributi (membri dati)Fondamenti di costruttori e distruttoriRiepilogo - Calcolatrice semplice4Proprietà delle classi
Membri di istanza e staticiGetter e setterFunzioni membro constParola chiave mutableMetodi e variabili staticiFunzioni e classi friendRiepilogo - Gestore di conti bancari7Ereditarietà
Ereditarietà di baseLivelli di accesso nell’ereditarietàOrdine di chiamata di costruttori e distruttoriRidefinizione dei metodiFunzioni virtuali e VTableEreditarietà multiplaEreditarietà virtualeRiepilogo - Gerarchia dei dipendenti2Gestione della memoria
Memoria Stack vs HeapPuntatori e riferimentiMemoria dinamica (new/delete)Puntatori intelligenti in C++RAII in C++Riepilogo - Gestore di array dinamico5Incapsulamento
Specificatori di accesso in C++Specificatori di accesso in dettaglioOccultamento delle informazioniStruct vs classClassi annidate e interneRiepilogo - Sistema di registrazione degli studenti8Polimorfismo
Polimorfismo a compile time e a runtimeOverload delle funzioniFunzioni virtuali: ripassoFunzioni virtuali pureClassi astratteProgettazione delle interfacce in C++Dynamic casting e RTTIRipasso: calcolatrice di forme11Concetti avanzati di OOP
Composizione vs ereditarietàMixin tramite CRTPIdiom PimplType ErasureEnum class e tipizzazione forteGestione delle eccezioni in OOPGerarchie personalizzate di eccezioni14Pattern di progettazione - Parte 2
Pattern CommandPattern AdapterPattern DecoratorPattern Template MethodPattern StatePattern CompositeRAII come pattern3Costruttori e distruttori
Costruttore predefinitoCostruttore con parametriCostruttore di copiaCostruttore di spostamentoListe di inizializzazione dei costruttoriCostruttori delegantiApprofondimento sui distruttoriRegola del Tre / Cinque / ZeroRipasso - classe String6Sovraccarico degli operatori
Introduzione al sovraccarico degli operatoriSovraccarico degli operatori aritmeticiSovraccarico degli operatori di confrontoOperatori di flussoSovraccarico dell'operatore di assegnazioneSovraccarico degli operatori [] e ()Operatori di conversione di tipoRipasso - Classe Matrix9Template
Template di funzioneTemplate di classeSpecializzazione dei templateTemplate variadiciBasi di SFINAE e dei trait di tipoRiepilogo - Contenitore generico12Funzionalità moderne di C++
Semantica di spostamento e rvaluePerfect forwardingEspressioni lambda in dettagliostd::function e std::bindconstexpr e constevalStructured bindingsoptional, variant, anyEsercitati da solo: Compilatore C++ online