Ready to practice?
Sign up to access interactive coding exercises and track your progress.
Conditional Move Optimization
Move only when guaranteed not to throw, otherwise copy safely.
std::move_if_noexcept
In the lesson on std::move, we covered how it casts its lvalue argument to an rvalue so that we can invoke move semantics. In the lesson on noexcept, we covered the noexcept exception specifier and operator. This lesson builds on both concepts.
We also covered the strong exception guarantee, which guarantees that if a function is interrupted by an exception, no memory will be leaked and the program state will not be changed. In particular, all constructors should uphold the strong exception guarantee so the rest of the program won't be left in an altered state if construction of an object fails.
The move constructors exception problem
Consider the case where we're copying an object, and the copy fails for some reason (e.g., insufficient memory). In such a case, the object being copied is not harmed because the source object doesn't need modification to create a copy. We can discard the failed copy and move on. The strong exception guarantee is upheld.
Now consider the case where we're moving an object instead. A move operation transfers ownership of resources from the source to the destination object. If the move operation is interrupted by an exception after the transfer of ownership occurs, our source object will be in a modified state. This isn't a problem if the source object is temporary and will be discarded after the move anyway—but for non-temporary objects, we've damaged the source object. To comply with the strong exception guarantee, we'd need to move the resource back to the source object, but if the move failed the first time, there's no guarantee the move back will succeed either.
How can we give move constructors the strong exception guarantee? It's simple to avoid throwing exceptions in the body of a move constructor, but a move constructor may invoke other constructors that are potentially throwing. Take for example the move constructor for std::pair, which must attempt to move each subobject from the source pair into the new pair object.
// Example move constructor definition for std::pair
// Take in an 'old' pair, and then move construct the new pair's 'first' and 'second' subobjects from the 'old' ones
template <typename T1, typename T2>
pair<T1,T2>::pair(pair&& old)
: first(std::move(old.first)),
second(std::move(old.second))
{}
Now let's use two classes, TransferableData and ReplicableData, which we'll pair together to demonstrate the strong exception guarantee problem with move constructors:
#include <iostream>
#include <utility>
#include <stdexcept>
class TransferableData
{
private:
int* m_resource{};
public:
TransferableData() = default;
TransferableData(int value)
: m_resource{ new int{value} }
{}
// Copy constructor
TransferableData(const TransferableData& other)
{
// deep copy
if (other.m_resource != nullptr)
{
m_resource = new int{*other.m_resource};
}
}
// Move constructor
TransferableData(TransferableData&& other) noexcept
: m_resource{other.m_resource}
{
other.m_resource = nullptr;
}
~TransferableData()
{
std::cout << "destroying " << *this << '\n';
delete m_resource;
}
friend std::ostream& operator<<(std::ostream& out, const TransferableData& data)
{
out << "TransferableData(";
if (data.m_resource == nullptr)
{
out << "empty";
}
else
{
out << *data.m_resource;
}
out << ')';
return out;
}
};
class ReplicableData
{
public:
bool m_throwOnCopy{};
ReplicableData() = default;
// Copy constructor throws an exception when copying from
// a ReplicableData object where m_throwOnCopy is true
ReplicableData(const ReplicableData& other)
: m_throwOnCopy{other.m_throwOnCopy}
{
if (m_throwOnCopy)
{
throw std::runtime_error{"Copy failed"};
}
}
};
int main()
{
// We can make a std::pair without any problems:
std::pair myPair{ TransferableData{42}, ReplicableData{} };
std::cout << "myPair.first: " << myPair.first << '\n';
// But the problem arises when we try to move that pair into another pair
try
{
myPair.second.m_throwOnCopy = true; // trigger copy constructor exception
// The following line will throw an exception
std::pair movedPair{ std::move(myPair) }; // We'll comment out this line later
// std::pair movedPair{ std::move_if_noexcept(myPair) }; // We'll uncomment this later
std::cout << "moved pair exists\n"; // never prints
}
catch (const std::exception& ex)
{
std::cerr << "Error: " << ex.what() << '\n';
}
std::cout << "myPair.first: " << myPair.first << '\n';
return 0;
}
The above program prints:
destroying TransferableData(empty) myPair.first: TransferableData(42) destroying TransferableData(42) Error: Copy failed myPair.first: TransferableData(empty) destroying TransferableData(empty)
Let's explore what happened. The first printed line shows the temporary TransferableData object used to initialize myPair gets destroyed as soon as the myPair instantiation statement executes. It's empty since the TransferableData subobject in myPair was move constructed from it, demonstrated by the next line showing myPair.first contains the TransferableData object with value 42.
The third line is where things get interesting. We created movedPair by copy constructing its ReplicableData subobject (it doesn't have a move constructor), but that copy construction threw an exception since we set the flag. Construction of movedPair was aborted by the exception, and its already-constructed members were destroyed. In this case, the TransferableData member was destroyed, printing destroying TransferableData(42). Next we see the Error: Copy failed message.
When we try to print myPair.first again, it shows the TransferableData member is empty. Since movedPair was initialized with std::move, the TransferableData member (which has a move constructor) was move constructed and myPair.first was left empty.
Finally, myPair was destroyed at the end of main().
To summarize: the move constructor of std::pair used the throwing copy constructor of ReplicableData. This copy constructor threw an exception, causing creation of movedPair to abort and myPair.first to be permanently damaged. The strong exception guarantee was not preserved.
std::move_if_noexcept to the rescue
Notice the above problem could have been avoided if std::pair had attempted a copy instead of a move. In that case, movedPair would have failed to construct, but myPair would not have been altered.
But copying instead of moving has a performance cost that we don't want to pay for all objects—ideally we want to do a move if we can do so safely, and a copy otherwise.
Fortunately, C++ has two mechanisms that, when used in combination, let us do exactly that. First, because noexcept functions are no-throw/no-fail, they implicitly meet the criteria for the strong exception guarantee. Thus, a noexcept move constructor is guaranteed to succeed.
Second, we can use the standard library function std::move_if_noexcept() to determine whether a move or a copy should be performed. std::move_if_noexcept is a counterpart to std::move, and is used the same way.
If the compiler can tell that an object passed as an argument to std::move_if_noexcept won't throw an exception when it is move constructed (or if the object is move-only and has no copy constructor), then std::move_if_noexcept will perform identically to std::move() (returning the object converted to an r-value). Otherwise, std::move_if_noexcept will return a normal l-value reference to the object.
`std::move_if_noexcept` will return a movable r-value if the object has a noexcept move constructor, otherwise it will return a copyable l-value. We can use the `noexcept` specifier in conjunction with `std::move_if_noexcept` to use move semantics only when a strong exception guarantee exists (and use copy semantics otherwise).
Let's update the code in the previous example as follows:
Running the program again prints:
destroying TransferableData(empty) myPair.first: TransferableData(42) destroying TransferableData(42) Error: Copy failed myPair.first: TransferableData(42) destroying TransferableData(42)
As you can see, after the exception was thrown, the subobject myPair.first still contains the value 42.
The move constructor of std::pair isn't noexcept (as of C++20), so std::move_if_noexcept returns myPair as an l-value reference. This causes movedPair to be created via the copy constructor (rather than the move constructor). The copy constructor can throw safely because it doesn't modify the source object.
The standard library uses std::move_if_noexcept often to optimize for functions that are noexcept. For example, std::vector::resize will use move semantics if the element type has a noexcept move constructor, and copy semantics otherwise. This means std::vector will generally operate faster with objects that have a noexcept move constructor.
If a type has both potentially throwing move semantics and deleted copy semantics (the copy constructor and copy assignment operator are unavailable), then `std::move_if_noexcept` will waive the strong guarantee and invoke move semantics. This conditional waiving of the strong guarantee is ubiquitous in the standard library container classes, since they use std::move_if_noexcept often.
Summary
The strong exception guarantee and move semantics: The strong exception guarantee promises that if a function is interrupted by an exception, no memory is leaked and program state isn't changed. Move operations can violate this guarantee because they modify the source object during the transfer.
The move constructor problem: If a move operation is interrupted by an exception after transferring ownership, the source object is left in a modified state, violating the strong exception guarantee. Moving back would require another move that might also fail.
Copy vs move for safety: Copying instead of moving preserves the strong exception guarantee because the source object isn't modified. However, copying has a performance cost we don't want to pay unnecessarily.
std::move_if_noexcept: This standard library function determines whether a move or copy should be performed. If the object has a noexcept move constructor, it returns a movable r-value. Otherwise, it returns a copyable l-value.
noexcept move constructors: Because noexcept functions are no-throw/no-fail, they implicitly meet the strong exception guarantee criteria. A noexcept move constructor is guaranteed to succeed.
Standard library usage: The standard library uses std::move_if_noexcept often to optimize for noexcept functions. For example, std::vector::resize uses move semantics if the element type has a noexcept move constructor, and copy semantics otherwise.
Performance benefits: Making move constructors noexcept allows standard library containers and algorithms to operate faster by using move semantics when safe, falling back to copy semantics only when necessary.
Understanding std::move_if_noexcept enables writing exception-safe code that leverages move semantics for performance while maintaining strong exception guarantees when possible.
Conditional Move Optimization - Quiz
Test your understanding of the lesson.
Practice Exercises
Move If Noexcept
Create classes with and without noexcept move constructors to understand how std::move_if_noexcept selects between move and copy operations for exception safety.
Lesson Discussion
Share your thoughts and questions
No comments yet. Be the first to share your thoughts!