Friendly reminder to mark your move constructors noexcept

^^^ The culprit to your performance losses ^^^

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:

  1. 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
  2. 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.

For example:

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.

For example:

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.

For example:

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.

Leave a comment