Since C++11 we have had the noexcept
keyword, which is a promise that the function will not throw an exception (and if it does, go straight to std::terminate
, do not pass go). noexcept
is nice for two reasons:
- The compiler can optimize a little better because it doesn’t need to emit any code for unwinding a call stack in case of an exception, and
- It leads to incredible performance differences at runtime for
std::vector
(and other containers, too)
We’re all familiar with std::vector
, our favorite wrapper around a contiguous resizable array. What you may not be familiar with is the fact that std::vector::push_back
(and emplace_back
) make "strong exception guarantees"
A strong exception guarantee is a guarantee that if emplace_back
should fail, the vector
is otherwise unchanged. For example:
struct MyClass
{
MyClass(int i)
{
if (i == 3)
throw std::invalid_argument("How dare you");
}
};
int main()
{
std::vector<MyClass> vec;
vec.emplace_back(1);
vec.emplace_back(2);
try{
vec.emplace_back(3); // throws
}catch(...){}
std::cout << vec.size() << '\n'; // 2
}
If emplace_back
causes the vector to reallocate, it can’t exactly std::move()
your elements to the new storage if the operation might throw, so it will copy them instead.
struct MyClass
{
MyClass(int i)
{
if (i == 3)
throw std::invalid_argument("How dare you");
}
MyClass(const MyClass&)
{
std::cout << "Copied\n";
}
MyClass(MyClass&&)
{
std::cout << "Moved\n";
}
};
int main()
{
std::vector<MyClass> vec;
vec.emplace_back(1);
vec.emplace_back(2); // outputs "Copied" after reallocating to store 2 elements
}
But when we mark our move constructor as noexcept
, suddenly we’re only performing moves on reallocation.
struct MyClass
{
MyClass(int i)
{
if (i == 3)
throw std::invalid_argument("How dare you");
}
MyClass(const MyClass&)
{
std::cout << "Copied\n";
}
MyClass(MyClass&&) noexcept
{
std::cout << "Moved\n";
}
};
int main()
{
std::vector<MyClass> vec;
vec.emplace_back(1);
vec.emplace_back(2); // outputs "Moved" after reallocating to store 2 elements
}
If no copy operation is available, it will begrudgingly use your throwing move constructor and forsake all exception guarantees.
struct MyClass
{
MyClass(int i)
{
if (i == 3)
throw std::invalid_argument("How dare you");
}
MyClass(const MyClass&) = delete;
MyClass(MyClass&&) noexcept(false)
{
std::cout << "Moved\n";
}
};
int main()
{
std::vector<MyClass> vec;
vec.emplace_back(1);
vec.emplace_back(2); // outputs "Moved" after reallocating to store 2 elements
}
Except MSVC didn’t start behaving this way until Visual Studio 2017!
The issue is historical (as most are) — when MSVC first got support for C++11, it didn’t support the noexcept
keyword at all, so programmers didn’t have a ton of control over this. (Well, there were dynamic exception specifications but nobody really used them).
Not wanting to sacrifice performance, Microsoft opted to bend their standards compliance for a while until they could have proper noexcept
support. I personally think Microsoft did a poor job of communicating this change; I cannot find a single article about it.
(However, I can forgive them due to the excellent performance profiler that comes with VS these days.)
I only found this issue when my team saw a big performance hit after upgrading our toolset to vs141, when I tracked it down to the surprisingly well-named function std::vector::_Umove_if_noexcept
.
So, be your project’s hero and add noexcept
to your move constructors wherever feasible. Better yet, let the compiler define your move constructor for you if possible.