Copy Control
Copy Control: The set of special member functions that control what happens when objects are copied, assigned, moved, and destroyed.
A class can control these operations through:
copy constructor
copy-assignment operator
move constructor
move-assignment operator
destructor
For many classes, compiler-generated versions are sufficient.
Classes that manage resources often need explicit copy-control behavior.
13.1 Copy, Assign, and Destroy
The three basic copy-control operations are:
- copy constructor
- copy-assignment operator
- destructor
Move operations are covered later in this chapter.
1) The Copy Constructor
Copy Constructor: A constructor whose first parameter is a reference to the class type and whose remaining parameters, if any, have default arguments.
Typical form:
class Foo
{
public:
Foo();
Foo(const Foo&);
};
The parameter is usually:
const Foo&
because copying should normally not modify the source object.
Copy Construction
Foo a;
Foo b(a);
or:
Foo b = a;
Both initialize a new object from an existing object.
Copy Initialization
Copy initialization is used in forms such as:
Foo b = a;
It is also used when an object is:
- passed to a function by value
- returned from a function by value
- used to initialize certain container elements or aggregates
Example:
void process(Foo value);
Foo object;
process(object);
Passing object by value may invoke the copy constructor.
Why the Parameter Must Be a Reference
Suppose the copy constructor took its argument by value:
// Wrong idea:
Foo(Foo other);
To initialize other, the program would need to copy a Foo.
That copy would need to call the copy constructor again.
Conceptually:
copy Foo
↓
initialize parameter Foo
↓
copy Foo
↓
initialize parameter Foo
↓
...
Therefore the copy constructor parameter must be a reference.
Synthesized Copy Constructor
If a class does not define its own copy constructor, the compiler normally synthesizes one.
The synthesized copy constructor performs memberwise copy.
Example:
class Data
{
public:
std::string name;
int value = 0;
};
Conceptually, copying:
Data a;
Data b = a;
copies:
a.name -> b.name
a.value -> b.value
Each member is copied according to its own type.
Pointer Members Are Copied as Pointer Values
Consider:
class HasPtr
{
public:
std::string* pointer;
};
The synthesized copy constructor copies only the pointer value.
Conceptually:
object A ----\
-> same string
object B ----/
It does not automatically allocate a new string.
Whether this behavior is correct depends on the intended semantics of the class.
Copy Constructor and explicit
Copy constructors are usually not declared explicit.
They are used implicitly in contexts where copying is required.
2) The Copy-Assignment Operator
Copy-Assignment Operator: Replaces the state of an existing object with a copy of another object of the same type.
Typical form:
class Foo
{
public:
Foo& operator=(
const Foo&);
};
Usage:
Foo a;
Foo b;
b = a;
Unlike construction, b already exists.
Assignment vs. Initialization
Initialization:
Foo b = a;
creates a new object.
Assignment:
b = a;
changes an already existing object.
These are different operations.
Return Type
Assignment operators normally return:
Foo&
and return:
*this
Example:
Foo&
Foo::operator=(
const Foo& rhs)
{
// assign members
return *this;
}
This permits chained assignment:
a = b = c;
Synthesized Copy-Assignment Operator
If the class does not define a copy-assignment operator, the compiler normally synthesizes one.
It assigns each non-static member from the corresponding member of the right-hand operand.
Example:
class Data
{
public:
std::string name;
int value = 0;
};
For:
a = b;
the synthesized operation behaves conceptually like:
a.name = b.name;
a.value = b.value;
Self-Assignment
An assignment operator should work correctly when:
object = object;
This is especially important for resource-managing classes.
A safe assignment implementation must not destroy the source data before it has been copied.
3) The Destructor
Destructor: A special member function that performs cleanup when an object is destroyed.
Syntax:
class Foo
{
public:
~Foo();
};
A destructor:
- has the class name preceded by
~ - has no return type
- takes no parameters
- cannot be overloaded
When Destructors Run
A destructor runs when an object ceases to exist.
Examples include:
local object leaves scope
container destroys an element
dynamically allocated object is deleted
temporary object is destroyed
Example:
void function()
{
Foo object;
}
When function() ends, object is destroyed.
Destructor Body and Member Destruction
A destructor body performs class-specific cleanup.
After the destructor body finishes, non-static data members are destroyed automatically.
Example:
class Record
{
public:
~Record()
{
// custom cleanup
}
private:
std::string name;
std::vector<int> values;
};
After the destructor body:
values destroyed
name destroyed
Class-type members run their own destructors automatically.
Pointer Members Are Not Automatically Deleted
Consider:
class HasPtr
{
private:
std::string* pointer;
};
Destroying the pointer member destroys only the pointer itself.
It does not automatically perform:
delete pointer;
If the class owns that allocation, the destructor must release it.
Synthesized Destructor
If no destructor is declared, the compiler normally synthesizes one.
Its function body is effectively empty, but member destruction still occurs automatically.
For classes composed entirely of self-managing members such as:
string
vector
shared_ptr
```
the synthesized destructor is often exactly what is needed.
---
### 4) The Rule of Three/Five
The copy-control operations should usually be considered together.
A useful rule is:
**If a class needs a destructor, it almost certainly also needs a copy constructor and copy-assignment operator.**
Why?
A custom destructor often means the class owns a resource.
If the compiler simply copies the resource handle, two objects may believe they own the same resource.
---
### Example of the Problem
~~~cpp
class HasPtr
{
public:
HasPtr(
const std::string& text)
: pointer(
new std::string(text))
{
}
~HasPtr()
{
delete pointer;
}
private:
std::string* pointer;
};
If the compiler-generated copy constructor is used:
HasPtr a("hello");
HasPtr b = a;
both objects receive the same pointer.
Conceptually:
a.pointer ----\
-> dynamic string
b.pointer ----/
When both destructors execute:
delete same pointer
delete same pointer again
```
which is invalid.
---
### Rule of Three
For classes that manage resources, the traditional three operations are:
~~~text
copy constructor
copy-assignment operator
destructor
If one is needed, the others often need consideration as well.
Rule of Five
C++11 adds:
move constructor
move-assignment operator
giving five copy-control operations in total.
Resource-managing classes should consider all five together.
5) Using = default
= default: Explicitly requests the compiler-generated version of a special member function.
class Data
{
public:
Data() = default;
Data(
const Data&) = default;
Data& operator=(
const Data&) = default;
~Data() = default;
};
This makes the intended behavior explicit.
In-Class = default
When defaulted inside the class:
Data(
const Data&) = default;
the generated function is implicitly inline.
Out-of-Class = default
A special member can also be defaulted outside the class.
class Data
{
public:
Data& operator=(
const Data&);
};
Data&
Data::operator=(
const Data&) = default;
6) Preventing Copies
Some objects should not be copyable.
Examples include objects representing unique resources such as:
- IO streams
- exclusive handles
- synchronization objects
C++11 uses deleted functions to express this directly.
= delete
class NoCopy
{
public:
NoCopy() = default;
NoCopy(
const NoCopy&) = delete;
NoCopy& operator=(
const NoCopy&) = delete;
};
Now:
NoCopy a;
// Error:
// NoCopy b = a;
// Error:
// b = a;
The interface clearly states that copying is forbidden.
Deleted Functions
A deleted function is declared but cannot be used.
void function(int) = delete;
For copy control, deleted functions are especially useful to prohibit copy or assignment.
Unlike = default, = delete must appear on the first declaration of the function.
Synthesized Deleted Operations
The compiler may define a synthesized special member as deleted when a member cannot perform the required operation.
For example, a synthesized copy-assignment operator may be deleted if the class contains:
const data member
reference data member
member whose assignment operator is deleted
```
The basic rule is:
~~~text
if a member cannot be copied,
assigned, or destroyed
the corresponding synthesized
class operation may be deleted
13.2 Copy Control and Resource Management
A resource-managing class must decide what copying means.
Two common semantic models are:
- value-like behavior
- pointer-like behavior
Value-Like Behavior
A value-like class gives each object its own independent resource.
Example:
object A -> string A
object B -> separate string B
Copying duplicates the resource.
Standard types such as:
string
vector
```
behave like values.
---
### Pointer-Like Behavior
A pointer-like class allows copies to share the same underlying resource.
Example:
~~~text
object A ----\
-> shared resource
object B ----/
shared_ptr and the earlier StrBlob example have pointer-like semantics.
1) Classes That Act Like Values
Consider:
class HasPtr
{
public:
HasPtr(
const std::string& text =
std::string())
: pointer(
new std::string(text)),
value(0)
{
}
private:
std::string* pointer;
int value;
};
To behave like a value, each HasPtr needs its own copy of the string.
Value-Like Copy Constructor
HasPtr::HasPtr(
const HasPtr& other)
: pointer(
new std::string(
*other.pointer
)),
value(other.value)
{
}
Conceptually:
source.pointer -> source string
copy.pointer -> separate copied string
The pointer value itself is not copied as the owned resource.
The pointed-to string is copied.
Value-Like Destructor
HasPtr::~HasPtr()
{
delete pointer;
}
Each object owns exactly one allocation.
Therefore each object releases its own allocation.
Value-Like Copy Assignment
Assignment must:
- copy the right-hand resource
- release the old left-hand resource
- install the copied resource
- copy ordinary members
A safe implementation is:
HasPtr&
HasPtr::operator=(
const HasPtr& rhs)
{
auto new_pointer =
new std::string(
*rhs.pointer
);
delete pointer;
pointer = new_pointer;
value = rhs.value;
return *this;
}
Why Copy Before Delete?
Suppose:
object = object;
If the assignment deleted pointer first, then rhs.pointer would also refer to deleted memory because rhs is the same object.
By allocating the new copy first:
copy source data
↓
delete old data
↓
install new data
```
self-assignment remains safe.
It also provides better exception behavior: if allocation throws, the original object is unchanged.
---
### 2) Classes That Act Like Pointers
A pointer-like class shares its resource among copies.
The easiest implementation is generally:
~~~cpp
std::shared_ptr<T>
because it already implements reference counting.
However, manual reference counting illustrates how shared ownership works.
Reference Counting
A reference count records how many objects share a resource.
Conceptually:
object A ----\
object B -----+--> shared resource
object C ----/ |
count = 3
Reference-Count Rules
The pattern is:
- new resource starts with count
1 - copy constructor increments the count
- destructor decrements the count
- last owner deletes the resource and count
- assignment increments the new count before releasing the old resource
Pointer-Like HasPtr
class HasPtr
{
public:
HasPtr(
const std::string& text =
std::string())
: pointer(
new std::string(text)),
value(0),
use(
new std::size_t(1))
{
}
HasPtr(
const HasPtr& other)
: pointer(other.pointer),
value(other.value),
use(other.use)
{
++*use;
}
HasPtr& operator=(
const HasPtr&);
~HasPtr();
private:
std::string* pointer;
int value;
std::size_t* use;
};
Pointer-Like Destructor
HasPtr::~HasPtr()
{
if (--*use == 0)
{
delete pointer;
delete use;
}
}
Only the last owner deletes the resource.
Pointer-Like Copy Assignment
HasPtr&
HasPtr::operator=(
const HasPtr& rhs)
{
++*rhs.use;
if (--*use == 0)
{
delete pointer;
delete use;
}
pointer = rhs.pointer;
value = rhs.value;
use = rhs.use;
return *this;
}
Why Increment First?
For self-assignment:
object = object;
the right-hand and left-hand counters are the same.
Incrementing first ensures the resource cannot be deleted before the assignment completes.
Conceptually:
increment rhs ownership
↓
release lhs ownership
↓
copy shared pointers
```
---
## 13.3 Swap
Resource-managing classes often define their own `swap`.
**swap:** Exchanges the states of two objects.
Generic algorithms frequently use swapping when reordering elements.
---
### Why a Custom `swap` Helps
A naive value-like swap may involve:
~~~text
copy object A
assign B to A
assign temporary to B
```
For a class that owns dynamically allocated memory, this can perform unnecessary allocations and copies.
A custom swap can exchange the resource handles instead.
---
### `HasPtr` Swap
~~~cpp
class HasPtr
{
friend void swap(
HasPtr&,
HasPtr&);
// ...
};
Definition:
void swap(
HasPtr& lhs,
HasPtr& rhs)
{
using std::swap;
swap(
lhs.pointer,
rhs.pointer
);
swap(
lhs.value,
rhs.value
);
}
Only pointer and integer values are exchanged.
The dynamically allocated strings themselves are not copied.
Why using std::swap
Inside a class-specific swap, write:
using std::swap;
swap(member1, other.member1);
swap(member2, other.member2);
rather than directly forcing:
std::swap(...)
This allows a class-specific swap for member types to participate when one exists.
Copy-and-Swap Assignment
A value-like class can implement assignment by taking the right-hand operand by value.
HasPtr&
HasPtr::operator=(
HasPtr rhs)
{
swap(*this, rhs);
return *this;
}
The parameter:
HasPtr rhs
is already a copy.
Then swap() exchanges the copied state with the current object.
When rhs is destroyed, it destroys the old state of *this.
Copy-and-Swap Flow
copy rhs into parameter
↓
swap parameter with *this
↓
*this owns new state
↓
parameter owns old state
↓
parameter destructor releases old state
This naturally handles self-assignment.
13.4 A Copy-Control Example
Copy control is not needed only for dynamic memory.
A class may need custom copy control because copying or destruction requires bookkeeping.
The chapter uses two cooperating classes:
Message
Folder
A Message may appear in several Folders.
Each side tracks the relationship.
Relationship Structure
Conceptually:
Message
|
+--> Folder A
|
+--> Folder B
Folder A
|
+--> Message
Folder B
|
+--> Message
If either side changes, the other side must also be updated.
Simplified Message
class Folder;
class Message
{
friend class Folder;
public:
explicit Message(
const std::string& text =
std::string())
: contents(text)
{
}
Message(
const Message&);
Message& operator=(
const Message&);
~Message();
void save(Folder&);
void remove(Folder&);
private:
std::string contents;
std::set<Folder*> folders;
void add_to_Folders(
const Message&);
void remove_from_Folders();
};
save()
When a message is saved in a folder:
- the message records the folder
- the folder records the message
Conceptually:
void Message::save(
Folder& folder)
{
folders.insert(&folder);
folder.addMsg(this);
}
remove()
Likewise removal updates both sides.
void Message::remove(
Folder& folder)
{
folders.erase(&folder);
folder.remMsg(this);
}
Copy Constructor Bookkeeping
A copied message should appear in the same folders as the original.
Helper:
void Message::add_to_Folders(
const Message& message)
{
for (auto folder
: message.folders)
{
folder->addMsg(this);
}
}
Copy constructor:
Message::Message(
const Message& message)
: contents(
message.contents),
folders(
message.folders)
{
add_to_Folders(
message
);
}
The new message gets the same folder pointers.
Each folder is then updated to include the new message.
Destructor Bookkeeping
When a message is destroyed, each folder must stop referring to it.
Helper:
void
Message::remove_from_Folders()
{
for (auto folder
: folders)
{
folder->remMsg(this);
}
folders.clear();
}
Destructor:
Message::~Message()
{
remove_from_Folders();
}
Copy Assignment Bookkeeping
Assignment must:
- remove the left-hand message from its current folders
- copy contents and folder relationships from the right-hand message
- add the left-hand message to the new folders
Message&
Message::operator=(
const Message& rhs)
{
remove_from_Folders();
contents =
rhs.contents;
folders =
rhs.folders;
add_to_Folders(rhs);
return *this;
}
The order matters.
Private Utility Functions
Copy assignment often combines work also needed by:
copy constructor
destructor
```
Common bookkeeping should be placed in private helper functions rather than duplicated.
---
## 13.5 Classes That Manage Dynamic Memory
The chapter next develops a simplified `vector<string>`-like class called `StrVec`.
Its purpose is to show how a class can directly manage:
- allocated storage
- object construction
- object destruction
- reallocation
- copy control
---
### `StrVec` Storage Model
`StrVec` uses three pointers.
~~~text
elements
|
v
[constructed][constructed][unused][unused]
^ ^
| |
first_free cap
Meanings:
elements
first constructed element
first_free
one past last constructed element
cap
one past allocated storage
Simplified Class Definition
class StrVec
{
public:
StrVec()
: elements(nullptr),
first_free(nullptr),
cap(nullptr)
{
}
StrVec(
const StrVec&);
StrVec& operator=(
const StrVec&);
~StrVec();
void push_back(
const std::string&);
std::size_t size() const
{
return
first_free
- elements;
}
std::size_t capacity() const
{
return
cap
- elements;
}
std::string* begin() const
{
return elements;
}
std::string* end() const
{
return first_free;
}
private:
static std::allocator<
std::string
> alloc;
void chk_n_alloc();
std::pair<
std::string*,
std::string*
>
alloc_n_copy(
const std::string*,
const std::string*);
void free();
void reallocate();
std::string* elements;
std::string* first_free;
std::string* cap;
};
push_back()
Before constructing a new element, ensure there is available storage.
void StrVec::push_back(
const std::string& text)
{
chk_n_alloc();
alloc.construct(
first_free++,
text
);
}
Checking Capacity
void StrVec::chk_n_alloc()
{
if (size() == capacity())
{
reallocate();
}
}
Reallocation occurs only when the allocated block is full.
alloc_n_copy()
Copy construction and assignment both need to allocate storage and copy a range.
std::pair<
std::string*,
std::string*
>
StrVec::alloc_n_copy(
const std::string* begin,
const std::string* end)
{
auto data =
alloc.allocate(
end - begin
);
return {
data,
std::uninitialized_copy(
begin,
end,
data
)
};
}
The result contains:
pointer to beginning
pointer one past copied elements
Copy Constructor
StrVec::StrVec(
const StrVec& other)
{
auto data =
alloc_n_copy(
other.begin(),
other.end()
);
elements =
data.first;
first_free =
cap =
data.second;
}
The copied object receives its own independently allocated elements.
This is value-like behavior.
free()
void StrVec::free()
{
if (elements)
{
for (auto pointer =
first_free;
pointer != elements;)
{
alloc.destroy(
--pointer
);
}
alloc.deallocate(
elements,
cap - elements
);
}
}
Objects are destroyed before the raw storage is deallocated.
Destructor
StrVec::~StrVec()
{
free();
}
Copy Assignment
The assignment should allocate the new data before releasing the old data.
StrVec&
StrVec::operator=(
const StrVec& rhs)
{
auto data =
alloc_n_copy(
rhs.begin(),
rhs.end()
);
free();
elements =
data.first;
first_free =
cap =
data.second;
return *this;
}
This handles self-assignment safely.
Reallocation
When capacity is exhausted, StrVec must:
- allocate a larger block
- construct elements in the new storage
- destroy old elements
- release old storage
- update internal pointers
Typical growth:
new capacity =
old size * 2
or 1 when empty
Basic Reallocation
void StrVec::reallocate()
{
auto new_capacity =
size()
? 2 * size()
: 1;
auto new_data =
alloc.allocate(
new_capacity
);
auto destination =
new_data;
auto source =
elements;
for (std::size_t i = 0;
i != size();
++i)
{
alloc.construct(
destination++,
*source++
);
}
free();
elements =
new_data;
first_free =
destination;
cap =
elements
+ new_capacity;
}
Later, move operations can make this transfer more efficient.
13.6 Moving Objects
Copying can be expensive when an object owns a large resource.
Sometimes the source object is temporary and will be destroyed immediately.
Instead of copying its resource, the program can transfer ownership.
This is the purpose of move semantics.
1) Rvalue References
Rvalue Reference: A reference declared with && that normally binds to an rvalue.
int&& reference = 42;
Ordinary lvalue references use:
int&
Lvalues and Rvalues
A simplified distinction:
Lvalue: An expression representing an object with a persistent identity.
std::string text = "hello";
text is an lvalue.
Rvalue: A temporary or disposable value.
std::string("hello")
is an rvalue.
Binding Rules
An ordinary non-const lvalue reference:
std::string& ref = text;
binds to an lvalue.
An rvalue reference:
std::string&& ref =
std::string("hello");
binds to an rvalue.
A reference to const can bind to either:
const std::string& ref =
std::string("hello");
Rvalue References Identify Movable Objects
An rvalue often represents an object whose current value will not be needed later.
Therefore a function receiving:
T&&
can often safely transfer resources from that object.
Variables Are Lvalues
A subtle rule:
Even if a variable's type is an rvalue reference, the variable expression itself is an lvalue.
std::string&& ref =
std::string("hello");
The expression:
ref
is an lvalue because it has a name and persistent identity.
std::move()
std::move() converts an lvalue expression into an rvalue expression suitable for move operations.
Required header:
#include <utility>
Example:
std::string source =
"hello";
std::string target =
std::move(source);
std::move() itself does not move data.
It changes the value category so that move-enabled overloads may be selected.
After std::move()
After moving from an object:
std::string target =
std::move(source);
source still exists.
It must remain:
valid
destructible
```
but its exact value should generally not be assumed.
Safe operations include:
~~~cpp
source.clear();
source = "new value";
Do not rely on its old contents.
Use std::move() Carefully
Moving is most appropriate when the source object's current value is no longer needed.
Casual use can make program state harder to understand.
Especially in ordinary application code, move only when ownership transfer is clearly intended.
2) Move Constructor and Move Assignment
Move Constructor: Initializes a new object by transferring resources from an rvalue object.
Typical form:
ClassName(
ClassName&&) noexcept;
Move-Assignment Operator: Replaces an existing object's state by transferring resources from an rvalue.
Typical form:
ClassName&
operator=(
ClassName&&) noexcept;
StrVec Move Constructor
StrVec::StrVec(
StrVec&& source) noexcept
: elements(
source.elements),
first_free(
source.first_free),
cap(
source.cap)
{
source.elements =
nullptr;
source.first_free =
nullptr;
source.cap =
nullptr;
}
No new storage is allocated.
The three pointers are transferred.
Move Constructor Flow
Before:
source
|
v
allocated strings
After:
destination
|
v
allocated strings
source
|
v
nullptr
The destination becomes responsible for the resource.
The source is left safe to destroy.
Moved-From Objects
After a move, the source object must remain valid and destructible.
However, unless the class specifies otherwise, its exact value is unspecified.
The key rule is:
valid state
but unspecified value
noexcept
Move operations that cannot throw should normally be declared:
noexcept
Example:
StrVec(
StrVec&&) noexcept;
If the definition is outside the class, noexcept must also appear there.
StrVec::StrVec(
StrVec&& source) noexcept
: ...
{
}
Why noexcept Matters
Containers such as vector may need to relocate elements.
If moving an element might throw, the container may choose copying instead to preserve exception guarantees.
A nonthrowing move operation lets the library move elements more aggressively.
Move Assignment
A move-assignment operator must release the old resource and take ownership of the new one.
StrVec&
StrVec::operator=(
StrVec&& rhs) noexcept
{
if (this != &rhs)
{
free();
elements =
rhs.elements;
first_free =
rhs.first_free;
cap =
rhs.cap;
rhs.elements =
nullptr;
rhs.first_free =
nullptr;
rhs.cap =
nullptr;
}
return *this;
}
The explicit self-check is important because the operation destroys the current resource.
Synthesized Move Operations
The compiler may synthesize move operations, but the rules are more restrictive than for copy operations.
Move operations are synthesized only when appropriate conditions are met.
In particular, defining your own:
copy constructor
copy-assignment operator
destructor
```
prevents automatic synthesis of move operations.
---
### Memberwise Move
When synthesized, move operations move each member according to its type.
Example:
~~~cpp
struct Data
{
int value;
std::string text;
};
The integer is transferred as a value.
The string uses its move operation.
Copy Can Substitute for Move
If a class has no move constructor but has a usable copy constructor:
T object2 =
std::move(object1);
may still compile.
The copy constructor can often bind to the rvalue through:
const T&
In that case, copying occurs instead of moving.
Move and Deleted Copy Operations
Declaring move operations affects synthesized copy operations.
A class that explicitly declares a move constructor or move-assignment operator may have its synthesized copy operations defined as deleted.
Therefore copy and move support must be designed together.
Move Iterators
Move Iterator: An iterator adaptor whose dereference operation produces an rvalue reference.
Create one with:
std::make_move_iterator(
iterator
)
Required header:
#include <iterator>
Moving During StrVec Reallocation
Instead of copying strings:
std::uninitialized_copy(
begin(),
end(),
new_data
);
use move iterators:
std::uninitialized_copy(
std::make_move_iterator(
begin()
),
std::make_move_iterator(
end()
),
new_data
);
The destination strings are then constructed from rvalue references.
This allows their move constructors to be selected.
Reallocation with Move Iterators
void StrVec::reallocate()
{
auto new_capacity =
size()
? 2 * size()
: 1;
auto first =
alloc.allocate(
new_capacity
);
auto last =
std::uninitialized_copy(
std::make_move_iterator(
begin()
),
std::make_move_iterator(
end()
),
first
);
free();
elements =
first;
first_free =
last;
cap =
elements
+ new_capacity;
}
Moving can avoid allocating and copying the internal character data of each string.
3) Rvalue References and Member Functions
Move semantics can also improve ordinary member functions.
A common pair of overloads is:
void push_back(
const T&);
void push_back(
T&&);
The first copies.
The second moves.
Copying push_back
void StrVec::push_back(
const std::string& text)
{
chk_n_alloc();
alloc.construct(
first_free++,
text
);
}
An lvalue argument selects this overload.
std::string text =
"hello";
vec.push_back(text);
Moving push_back
void StrVec::push_back(
std::string&& text)
{
chk_n_alloc();
alloc.construct(
first_free++,
std::move(text)
);
}
An rvalue selects this overload.
vec.push_back(
"hello"
);
The temporary string can be moved into the container.
Why std::move(text) Is Needed Inside
Although the parameter type is:
std::string&&
the named parameter:
text
is an lvalue expression.
Therefore:
std::move(text)
is required to pass it onward as an rvalue.
Copy/Move Overload Pattern
Functions that distinguish copying from moving typically use:
const T&
```
for copying and:
~~~cpp
T&&
for moving.
A const T&& is usually not useful for moving because moving normally modifies the source.
Reference-Qualified Member Functions
Member functions can restrict whether they may be called on lvalue or rvalue objects.
Use:
&
for lvalue objects.
Use:
&&
for rvalue objects.
Lvalue-Qualified Assignment
class Foo
{
public:
Foo& operator=(
const Foo&) &;
};
This assignment operator may be called only when the left-hand object is an lvalue.
Foo a;
Foo b;
a = b;
Valid.
An assignment to a temporary result can be rejected.
Reference Qualifier Syntax
void function() &;
void function() &&;
A reference qualifier:
- appears after the parameter list
- applies only to non-static member functions
- must appear in both declaration and definition
Combining const and Reference Qualifiers
The order is:
const &
or:
const &&
Example:
Foo sorted() const &;
Overloading on Reference Qualification
class Foo
{
public:
Foo sorted() &&;
Foo sorted() const &;
private:
std::vector<int> data;
};
The rvalue overload may modify the temporary directly.
Foo
Foo::sorted() &&
{
std::sort(
data.begin(),
data.end()
);
return *this;
}
Lvalue/Const Version
Foo
Foo::sorted() const &
{
Foo result(*this);
std::sort(
result.data.begin(),
result.data.end()
);
return result;
}
For an lvalue, the function sorts a copy instead of modifying the original.
Overload Selection
If:
Foo make_foo();
Foo object;
then:
make_foo().sorted();
calls the && version because the temporary result is an rvalue.
object.sorted();
calls the const & version because object is an lvalue.
Essential Study Checklist
- Copy control defines what happens when objects are copied, assigned, moved, and destroyed.
- The five copy-control members are the copy constructor, copy assignment, move constructor, move assignment, and destructor.
- A copy constructor normally takes
const T&. - Copy initialization can invoke the copy constructor.
- Passing or returning an object by value may invoke copy construction.
- The synthesized copy constructor performs memberwise copy.
- Pointer members are copied as pointer values by synthesized copy operations.
- Copy assignment modifies an already existing object.
- Copy-assignment operators normally return
*thisby reference. - Assignment operators must handle self-assignment correctly.
- A destructor performs cleanup when an object ceases to exist.
- Class-type members are destroyed automatically after the destructor body.
- Destroying a pointer member does not delete the pointed-to object.
- If a class needs a destructor, it usually needs explicit copy operations as well.
- Resource-managing classes should consider the Rule of Three/Five.
= defaultrequests the compiler-generated special member.= deleteexplicitly forbids an operation.- A synthesized copy-control member may be deleted when a member cannot perform the corresponding operation.
- Value-like classes duplicate their owned resources when copied.
- Pointer-like classes allow copied objects to share a resource.
- A value-like copy constructor must deep-copy owned resources.
- Value-like assignment should copy new data before releasing old data.
- Reference counting allows pointer-like objects to manage shared resources manually.
- The last reference-counted owner destroys the shared resource.
- A custom
swapcan exchange resource handles without expensive deep copies. using std::swap; swap(a, b);allows member-specific swaps to participate.- Copy-and-swap assignment naturally handles self-assignment.
- Classes may need copy control for bookkeeping even when they do not directly manage dynamic memory.
- Copying a
Messagemust update every associatedFolder. - Destroying a
Messagemust remove references to it from associated folders. StrVecdemonstrates manual allocation, construction, destruction, and reallocation.allocatorseparates raw storage allocation from object construction.- A move operation transfers resources rather than duplicating them.
- An rvalue reference is written as
T&&. std::move()converts an expression so move-enabled overloads may be selected.- A named rvalue-reference variable is itself an lvalue expression.
- A moved-from object remains valid and destructible, but its value is generally unspecified.
- Move constructors should leave the source safe to destroy.
- Nonthrowing move operations should be declared
noexcept. noexcepthelps containers safely prefer moving over copying.- Move assignment must release the destination's old resource before taking the source resource.
- User-declared copy-control members can prevent synthesis of move operations.
- If no move operation exists, a copy operation may be used for an rvalue.
- Move iterators produce rvalue references when dereferenced.
make_move_iterator()can let algorithms move elements instead of copying them.- Copy/move overload pairs normally use
const T&andT&&. - The rvalue-reference overload may transfer resources from its argument.
- A named
T&¶meter usually needsstd::move()when passed onward for moving. - Reference-qualified member functions can distinguish lvalue and rvalue objects.
&restricts a member to lvalue objects, while&&restricts it to rvalue objects.constappears before a reference qualifier, as inconst &.- Reference qualification can be used to provide different behavior for persistent objects and disposable temporaries.