Why do I get a error if I don't have a custom assignment member?

Brugerdefinerede Tildelingsoperatorer i C++

18/10/2024

Rating: 4.72 (15832 votes)

I C++-verdenen er tildelingsoperatorer fundamentale værktøjer, der bruges til at ændre værdien af et objekt. Selvom de kan virke simple ved første øjekast, især den grundlæggende tildelingsoperator (=), åbner de op for en verden af kompleksitet og kontrol, når man arbejder med brugerdefinerede klasser. At forstå, hvornår og hvorfor man skal implementere en brugerdefineret tildelingsoperator, er afgørende for at skrive sikker, effektiv og forudsigelig kode. Uden en korrekt forståelse kan man let introducere subtile fejl, hukommelseslækager eller endda ubestemt adfærd (undefined behavior), som kan være notorisk svære at fejlsøge. Denne artikel vil udforske de dybere aspekter af tildelingsoperatorer, fra de grundlæggende typer til avancerede scenarier med const-medlemmer og ydelsesovervejelser.

Can a class with const member be modified?
Consider the class with const member: const int c; // must not be modified! A(int c) : c(c) {} A(const A& copy) : c(copy.c) { } // No assignment operator I want to have an assignment operator but I do not want to use const_cast like in the following code from one of the answers:
Indholdsfortegnelse

Hvorfor er en Brugerdefineret Tildelingsoperator Nødvendig?

For simple datatyper som int eller double er tildeling ligetil. Men når vi arbejder med klasser, især dem, der administrerer ressourcer som dynamisk allokeret hukommelse, filhåndtag eller netværksforbindelser, bliver billedet mere kompliceret. Compileren genererer automatisk en standard tildelingsoperator for dine klasser, hvis du ikke selv definerer en. Denne standardoperator udfører en medlem-for-medlem kopi, hvilket betyder, at den kopierer værdien af hvert medlem fra kildeobjektet til destinationsobjektet.

Dette er ofte tilstrækkeligt, men det kan føre til alvorlige problemer i visse situationer. Forestil dig en klasse med en pointer som datamedlem. En medlem-for-medlem kopi vil kun kopiere pointerens adresse, ikke de data, den peger på. Begge objekter vil nu pege på den samme hukommelsesblok. Dette kaldes en 'shallow copy'. Problemet opstår, når et af objekternes destruktor kaldes og frigiver hukommelsen. Det andet objekt vil nu have en 'dangling pointer', der peger på frigivet hukommelse, hvilket uundgåeligt fører til nedbrud eller uforudsigelig opførsel. Her er en brugerdefineret tildelingsoperator, der udfører en 'deep copy' (kopierer de faktiske data), essentiel.

Et andet centralt scenarie opstår, når en klasse indeholder const-medlemmer. Compileren kan ikke generere en standard tildelingsoperator for en sådan klasse, fordi et const-medlem ikke kan ændres efter initialisering. Hvis du forsøger at tildele til en instans af en sådan klasse, vil du få en kompileringsfejl. Dette tvinger udvikleren til at overveje, hvad tildeling egentlig betyder for dette objekt. Skal det overhovedet være muligt at tildele? Hvis ja, hvordan skal det implementeres?

De Forskellige Typer af Tildelingsoperatorer

C++ tilbyder en række tildelingsoperatorer, som kan opdeles i to hovedkategorier: simpel tildeling og sammensat tildeling.

Why do I need a custom assignment operator?
This is because the assignment operator, which typically assigns all member variables, cannot modify const members once they're initialized. Therefore, when a class has const member variables and requires object assignment, a custom assignment operator is necessary. This custom operator should handle the assignment of non-const members as needed.

Simpel Tildeling

Den simple tildelingsoperator (=) er den mest grundlæggende. Dens formål er at erstatte indholdet af objektet på venstre side med en kopi af indholdet af objektet på højre side. For klassetyper udføres dette af en speciel medlemsfunktion, kendt som kopitildelingsoperatoren (copy assignment operator) eller, siden C++11, flyttetildelingsoperatoren (move assignment operator).

  • Kopitildeling (Copy Assignment): Erstatter indholdet af objekt a med en kopi af indholdet af b. Objekt b forbliver uændret. Dette kan være en dyr operation for store objekter.
  • Flyttetildeling (Move Assignment): Introduceret i C++11. Erstatter indholdet af objekt a med indholdet af b, men i stedet for at kopiere, 'stjæler' den ressourcerne fra b. Dette er langt mere effektivt, da det undgår unødvendige kopier. Objekt b efterlades i en gyldig, men uspecificeret tilstand.

Sammensat Tildeling

Sammensatte tildelingsoperatorer udfører en aritmetisk, bitvis eller skifteoperation, før resultatet gemmes. Disse operatorer, som +=, -=, *=, /=, er syntaktisk sukker for a = a op b, men med en vigtig forskel: udtrykket på venstre side evalueres kun én gang, hvilket kan give en lille ydelsesfordel.

Her er en oversigt over de mest almindelige sammensatte tildelingsoperatorer:

OperatorBetydning
+=Additionstildeling
-=Subtraktionstildeling
*=Multiplikationstildeling
/=Divisionstildeling
%=Modulustildeling
&=Bitvis AND-tildeling
|=Bitvis OR-tildeling
^=Bitvis XOR-tildeling
<<=Bitvis venstre-skift-tildeling
>>=Bitvis højre-skift-tildeling

Håndtering af 'const' og 'volatile' Medlemmer: En Farlig Sti

Som nævnt kan const-medlemmer forhindre tildeling. Nogle udviklere forsøger at omgå dette ved hjælp af teknikker som const_cast eller 'placement new'. Disse metoder er dog ekstremt farlige og fører næsten altid til ubestemt adfærd.

What is a simple assignment operator?
The simple assignment operator (=) causes the value of the second operand to be stored in the object specified by the first operand. If both objects are of arithmetic types, the right operand is converted to the type of the left, before storing the value.

At bruge const_cast til at fjerne const-kvalifikatoren fra et objekt, der oprindeligt blev erklæret som const, og derefter skrive til det, er eksplicit defineret som ubestemt adfærd i C++-standarden. Årsagen er, at compileren har lov til at udføre aggressive optimeringer baseret på antagelsen om, at en const-værdi aldrig ændrer sig. Den kan f.eks. cache værdien i et register eller erstatte alle læsninger af variablen med dens oprindelige værdi. Selvom du manuelt ændrer værdien i hukommelsen, vil dit program muligvis fortsætte med at bruge den gamle, cachede værdi, hvilket fører til ulogisk opførsel.

En interessant observation er dog brugen af nøgleordet volatile. Nøgleordet volatile fortæller compileren, at en variabels værdi kan ændre sig på uforudsigelige måder, som compileren ikke kan forudse (f.eks. via hardware-interrupts i indlejret programmering). Dette tvinger compileren til at undgå optimeringer og altid læse værdien direkte fra hukommelsen ved hver adgang. Hvis et medlem erklæres som volatile const, bliver det teknisk set muligt at bruge const_cast til at skrive til det, og efterfølgende læsninger vil se den nye værdi. Dette er en teknik, der undertiden ses i lavniveausystemer til at interagere med read-only hardware-registre, der kan ændres af hardwaren selv. Det er dog ikke en anbefalet praksis for almindelig applikationskode, da det bryder med de grundlæggende garantier, som const er designet til at give, og gør koden sværere at forstå og vedligeholde.

Ydelsesovervejelser og Hukommelsesstyring

Valget af datatyper og implementeringen af tildelingsoperatorer har en direkte indvirkning på et programs ydeevne og hukommelsesforbrug. At spilde plads er stadig en dårlig praksis, selv på moderne hardware med gigabytes af RAM. Når man arbejder med store dataindsamlinger (containere som std::vector eller std::map), bliver størrelsen af hvert enkelt element kritisk.

For eksempel kan en container med 64-bit heltal, der kun indeholder værdier i tusindvis, føre til oppustede binære filer, flere 'page faults' (når data skal hentes fra disk til RAM) og generelt dårligere ydeevne. Dette skyldes, at større objekter optager mere plads i CPU'ens cache, hvilket fører til flere 'cache misses' og langsommere adgang til hukommelsen. CPU'en er i dag så hurtig, at båndbredden til hukommelsen ofte er den reelle flaskehals.

What are assignment operators in C++ 11?
(C++11) Assignment operators modify the value of the object. All built-in assignment operators return *this, and most user-defined overloads also return *this so that the user-defined operators can be used in the same manner as the built-ins. However, in a user-defined operator overload, any type can be used as return type (including void).

Her spiller effektive tildelingsoperatorer en stor rolle. En velimplementeret flyttetildelingsoperator kan dramatisk forbedre ydeevnen ved at undgå dyre kopier af store objekter, når de flyttes rundt i containere eller returneres fra funktioner. Ved at 'stjæle' ressourcerne i stedet for at kopiere dem, reduceres operationen til blot at omarrangere nogle få pointere, hvilket er en ekstremt hurtig operation uanset objektets størrelse.

Sammenligning: Kopi- vs. Flyttetildeling

EgenskabKopitildeling (Copy Assignment)Flyttetildeling (Move Assignment)
FormålSkaber en dyb, uafhængig kopi af kildeobjektet.Overfører ejerskab af ressourcer fra kilde til destination.
Kildeobjektets TilstandForbliver uændret og fuldt funktionsdygtigt.Efterlades i en gyldig, men uspecificeret tilstand.
YdeevneKan være langsom for store objekter med mange ressourcer.Meget hurtig; kompleksiteten afhænger ikke af ressourcernes størrelse.
AnvendelseNår en ægte, separat kopi er nødvendig.Når kildeobjektet er midlertidigt (en rvalue) og ikke skal bruges igen.

Ofte Stillede Spørgsmål (OSS)

Hvad er "Reglen om Tre/Fem" (The Rule of Three/Five)?

Dette er en tommelfingerregel i C++. "Reglen om Tre" siger, at hvis en klasse definerer en af følgende (destruktor, kopikonstruktør eller kopitildelingsoperator), bør den sandsynligvis definere dem alle tre. Dette skyldes, at behovet for en af dem ofte indikerer, at klassen administrerer en ressource. "Reglen om Fem" udvider dette til C++11 ved at tilføje flyttekonstruktøren og flyttetildelingsoperatoren til listen.

Er det nogensinde sikkert at bruge `const_cast` til at ændre et `const`-objekt?

Nej, det er generelt ikke sikkert. Hvis objektet oprindeligt blev erklæret som `const`, resulterer forsøg på at ændre det via `const_cast` i ubestemt adfærd. Den eneste legitime brug af `const_cast` er at fjerne `const` fra en reference eller pointer, der peger på et objekt, som *ikke* oprindeligt var `const`. Dette kan være nødvendigt for at kalde en gammel API, der ukorrekt tager en ikke-`const` pointer.

What is a C++ object representation requirement?
The intent of this requirement is to preserve binary compatibility between the C++ library complex number types and the C language complex number types (and arrays thereof), which have an identical object representation requirement.

Hvad er forskellen mellem initialisering og tildeling?

Initialisering sker, når et objekt oprettes for første gang (f.eks. UserType B = A;). Dette kalder en konstruktør. Tildeling sker, når et eksisterende objekt får en ny værdi (f.eks. B = A; efter at B allerede er oprettet). Dette kalder tildelingsoperatoren.

Hvorfor returnerer tildelingsoperatorer normalt en reference til `*this`?

At returnere *this som en reference (f.eks. T& T::operator=(const T& other)) tillader 'chaining' af tildelinger, som f.eks. a = b = c;. Dette er i overensstemmelse med opførslen af de indbyggede typer og er en stærk konvention i C++.

Afslutningsvis er en grundig forståelse af tildelingsoperatorer ikke kun en akademisk øvelse; det er en praktisk nødvendighed for enhver seriøs C++-udvikler. Korrekt implementering af disse operatorer er nøglen til at skabe klasser, der er både sikre og effektive, og som opfører sig intuitivt. Ved at respektere principper som Reglen om Fem og undgå farlige genveje som const_castægte const-objekter, kan man bygge robust software, der er let at vedligeholde og fri for uventede fejl.

Hvis du vil læse andre artikler, der ligner Brugerdefinerede Tildelingsoperatorer i C++, kan du besøge kategorien Sundhed.

Go up