What Is std::move_if_noexcept?

std::move_if_noexcept is a second cast that lives next to std::move in <utility>, and it differs from it in exactly one respect: it looks at the type first. If that type's move constructor carries noexcept, the cast produces an r-value reference and a move follows, just as std::move would have arranged. If the move constructor makes no such promise and the type can be copied instead, the cast produces a reference to const, which no move constructor will accept, so a copy follows.

The decision is made from the type alone, while the program is being compiled. Nothing is inspected at run time, no exception is caught, and the object's contents never enter into it. Written out, it is a drop-in for the cast you already know: one argument, no template arguments to supply, usable at every site where std::move would go.

This lesson starts from that selection rule, shows it happening, and only then works backwards to the accident it is there to prevent.

The Verdict Is Visible in the Return Type

Three types, differing only in how their constructors are declared, are enough to see all of the behaviour. Filmstrip promises a non-throwing move. GlassSlide makes no promise but can be copied. MasterNegative makes no promise and has had copying deleted outright.

#include <cstddef>
#include <iostream>
#include <string>
#include <type_traits>
#include <utility>

class Filmstrip
{
public:
    explicit Filmstrip(std::string caption) : m_caption{ std::move(caption) } {}

    Filmstrip(const Filmstrip& other) : m_caption{ other.m_caption }
    {
        std::cout << "Filmstrip duplicated\n";
    }

    Filmstrip(Filmstrip&& other) noexcept : m_caption{ std::move(other.m_caption) }
    {
        std::cout << "Filmstrip relocated\n";
    }

    std::size_t captionLength() const { return m_caption.size(); }

private:
    std::string m_caption{};
};

class GlassSlide
{
public:
    explicit GlassSlide(std::string caption) : m_caption{ std::move(caption) } {}

    GlassSlide(const GlassSlide& other) : m_caption{ other.m_caption }
    {
        std::cout << "GlassSlide duplicated\n";
    }

    GlassSlide(GlassSlide&& other) : m_caption{ std::move(other.m_caption) }
    {
        std::cout << "GlassSlide relocated\n";
    }

    std::size_t captionLength() const { return m_caption.size(); }

private:
    std::string m_caption{};
};

class MasterNegative
{
public:
    explicit MasterNegative(std::string caption) : m_caption{ std::move(caption) } {}

    MasterNegative(const MasterNegative&) = delete;

    MasterNegative(MasterNegative&& other) : m_caption{ std::move(other.m_caption) }
    {
        std::cout << "MasterNegative relocated\n";
    }

    std::size_t captionLength() const { return m_caption.size(); }

private:
    std::string m_caption{};
};

int main()
{
    Filmstrip reel{ "reel 4" };
    GlassSlide slide{ "slide 9" };
    MasterNegative master{ "master 1" };

    static_assert(std::is_same_v<decltype(std::move_if_noexcept(reel)), Filmstrip&&>);
    static_assert(std::is_same_v<decltype(std::move_if_noexcept(slide)), const GlassSlide&>);
    static_assert(std::is_same_v<decltype(std::move_if_noexcept(master)), MasterNegative&&>);

    Filmstrip filedReel{ std::move_if_noexcept(reel) };
    GlassSlide filedSlide{ std::move_if_noexcept(slide) };
    MasterNegative filedMaster{ std::move_if_noexcept(master) };

    std::cout << "filed caption lengths: " << filedReel.captionLength() << ' '
              << filedSlide.captionLength() << ' ' << filedMaster.captionLength() << '\n';
    std::cout << "source caption lengths: " << reel.captionLength() << ' '
              << slide.captionLength() << ' ' << master.captionLength() << '\n';

    return 0;
}

Output:

Filmstrip relocated
GlassSlide duplicated
MasterNegative relocated
filed caption lengths: 6 7 8
source caption lengths: 0 7 0

The three static_assert lines are the important part, because they are checked by the compiler rather than by the program. They spell out the verdict for each type before a single object exists: Filmstrip&& and MasterNegative&& for the two that will be relocated, const GlassSlide& for the one that will be duplicated. The three constructions underneath simply act on those verdicts, and the announcements confirm which constructor ran.

Read the last line for the consequence. Both sources that went through a move report a caption length of zero, while the slide still has its seven characters. A moved-from std::string is left valid but with unspecified contents, so those zeros are what this library happened to do rather than a guarantee. What matters is that the two relocated sources no longer hold what they held, and the duplicated one does.

Reading the Three Verdicts

Move constructor of the type Copy constructor Cast hands back Constructor that runs
Declared noexcept present or not T&& move
Not noexcept usable const T& copy
Not noexcept deleted or otherwise unusable T&& move

Only the middle row differs from what plain std::move would have given you. The top row is the case worth aiming for, and the bottom row is a compromise the next-to-last section returns to.

Which row your own type sits on is not always a matter of what you wrote. A move constructor you did not spell out by hand computes its own specification from its members, so a defaulted one earns noexcept only when moving every member earns it. The rule composes upwards through the library too: std::pair and std::tuple promise a non-throwing move exactly when every element type does. One member with a potentially-throwing move is therefore enough to drop the enclosing class from the top row to the middle one, which is worth checking before you conclude that your class is being copied for some more exotic reason.

Key Concept
The question the cast asks is not "can this move fail?" but "has this type's author declared that it cannot?". noexcept on the move constructor is the entire input. A move constructor that could never throw in practice, but was written without the specifier, is treated as if it might.

Why the Middle Row Exists

A copy reads its source and writes somewhere else. Nothing about the original changes, so an exception partway through leaves you with a half-built destination to throw away and an original that is exactly as good as it was. That is the strong guarantee: the operation either finishes or leaves no trace.

A move is built the other way round. Making the destination cheap is the point, and it is cheap precisely because the destination takes over what the source was holding, which requires writing to the source. An exception raised after that hand-over has begun leaves the source hollowed out and the destination unfinished. Neither object holds the original data intact.

There is no clean recovery from that position. Putting the resource back would mean performing another move in the opposite direction, and the operation that just failed is the one you would be relying on to succeed.

This is why a noexcept move constructor is worth more than a merely fast one. noexcept declares the no-throw or no-fail level, which sits above the strong guarantee, so a move carrying it cannot leave anything half-done. The problem the middle row of the table solves is what to do about all the move constructors that carry no such declaration.

Watching a Shelf Get Damaged

Nothing above requires an exotic type. Relocating a batch of objects into fresh storage, which is what a growing container does constantly, is enough to expose the whole problem.

Plate owns a caption and, in both of its constructors, claims a slot from a shelf of fixed size. When the shelf is full, the constructor throws before it has touched anything. Its move constructor is not marked noexcept, which is the honest declaration here, since it genuinely can throw.

#include <cstddef>
#include <iostream>
#include <stdexcept>
#include <string>
#include <string_view>
#include <utility>
#include <vector>

class Plate
{
public:
    explicit Plate(std::string emulsion) : m_emulsion{ std::move(emulsion) } {}

    Plate(const Plate& other)
    {
        claimShelfSlot();
        m_emulsion = other.m_emulsion;
    }

    Plate(Plate&& other)
    {
        claimShelfSlot();
        m_emulsion = std::move(other.m_emulsion);
    }

    std::size_t emulsionLength() const { return m_emulsion.size(); }

    static void setShelfSlots(int slots) { s_shelfSlots = slots; }

private:
    static void claimShelfSlot()
    {
        if (s_shelfSlots <= 0)
        {
            throw std::length_error{ "shelf is full" };
        }

        --s_shelfSlots;
    }

    static inline int s_shelfSlots{ 0 };

    std::string m_emulsion{};
};

std::vector<Plate> freshShelf()
{
    Plate::setShelfSlots(3);

    std::vector<Plate> shelf{};
    shelf.reserve(3);
    shelf.push_back(Plate{ "orthochromatic" });
    shelf.push_back(Plate{ "panchromatic" });
    shelf.push_back(Plate{ "infrared" });

    return shelf;
}

void reportLengths(std::string_view label, const std::vector<Plate>& shelf)
{
    std::cout << label;

    for (const Plate& plate : shelf)
    {
        std::cout << ' ' << plate.emulsionLength();
    }

    std::cout << '\n';
}

void transfer(bool guarded)
{
    std::vector<Plate> shelf{ freshShelf() };
    Plate::setShelfSlots(2);

    std::vector<Plate> archive{};
    archive.reserve(shelf.size());

    try
    {
        for (Plate& plate : shelf)
        {
            if (guarded)
            {
                archive.push_back(std::move_if_noexcept(plate));
            }
            else
            {
                archive.push_back(std::move(plate));
            }
        }
    }
    catch (const std::length_error& error)
    {
        std::cout << "  transfer stopped: " << error.what() << '\n';
    }

    reportLengths("  shelf lengths:", shelf);
    reportLengths("  archive lengths:", archive);
}

int main()
{
    std::cout << "using std::move\n";
    transfer(false);

    std::cout << "using std::move_if_noexcept\n";
    transfer(true);

    return 0;
}

Output:

using std::move
  transfer stopped: shelf is full
  shelf lengths: 0 0 8
  archive lengths: 14 12
using std::move_if_noexcept
  transfer stopped: shelf is full
  shelf lengths: 14 12 8
  archive lengths: 14 12

Both runs fail in the same place and for the same reason: the shelf only had room for two of the three transfers. The archives come out identical, and in both cases the third plate never made it. The difference is entirely in what is left behind.

Under std::move, the first two plates were emptied into the archive before the third construction ran out of room, and the reported lengths 0 0 8 say so. The exception was caught, the program carried on, and the shelf it carried on with is now missing two captions that nothing will restore. That is the strong guarantee broken, in a program that never looks like it is doing anything dangerous.

Under std::move_if_noexcept, Plate matched the middle row of the table, so push_back took its const Plate& overload and copied. The archive cost more to build. In exchange, the failure changed nothing: 14 12 8 is the shelf exactly as it started. The operation still did not succeed, and it was never going to. What the cast bought is that the failure is now recoverable.

When the Guarantee Is Waived

The bottom row of the table is the awkward one, and MasterNegative in the first example is an instance of it. A type whose move constructor may throw and whose copy constructor is unavailable leaves the cast with no safe option to fall back on. Rather than refuse to compile, it hands back an r-value reference and the move goes ahead.

Warning
For a move-only type with a potentially-throwing move constructor, std::move_if_noexcept does not protect the source. It moves, and the strong guarantee is surrendered without a diagnostic. Because the standard containers reach for this cast constantly, that surrender is built into their behaviour too. The way out is not to avoid move-only types, it is to make sure their move operations are declared noexcept so that the top row of the table applies instead.

Where the Standard Library Applies This

You will rarely write std::move_if_noexcept yourself. You will run into it constantly all the same, because the containers use it on your behalf whenever they relocate elements: std::vector::resize, std::vector::reserve, and any push_back that has to grow the buffer all move existing elements into fresh storage, which is the exact situation the previous example modelled by hand.

The consequence is a performance cliff that turns on one keyword in your class. Give the element type a noexcept move constructor and every reallocation relocates pointers. Leave the specifier off and the same reallocation deep-copies every element, then destroys the originals, and does so silently. The container is not being cautious for its own sake; without the declaration it has no way to promise you the vector survives a failed growth.

Best Practice
Declare move constructors and move assignment operators noexcept. For the usual implementation, which copies a few pointers and blanks out the source, the promise costs nothing to keep, and it is what admits your type to the fast path in every container that stores it.

Key Terminology

  • std::move_if_noexcept: the cast in <utility> that consults a type's move constructor before choosing between an r-value reference and a reference to const.
  • Potentially-throwing: what a function is, by default, when nothing declares otherwise. The cast treats any move constructor lacking noexcept this way, regardless of what the body actually does.
  • Strong guarantee: the promise that a failed operation leaves the program's observable state as it was. Copies keep it by construction, moves need help.
  • Reallocation: a container abandoning its buffer for a larger one and relocating every element. The main place this cast runs without you writing it.
  • Guarantee waiver: what happens on the bottom row of the table, where no copy is available and the cast moves anyway.

Looking Forward

This lesson closes the loop opened two lessons ago. noexcept looked at first like documentation, a note to callers about what a function promises. Here it turns out to be a value the library reads and acts on, and the difference between a vector that relocates by pointer and one that deep-copies every element on every growth comes down to whether you wrote the word.

The chapter recap that follows collects the exception machinery into one view. Carry this piece of it forward: when you write a class that owns a resource, the move operations get noexcept, and it is not a stylistic preference.

Summary

What the cast decides: it reads the argument's type, and yields T&& when that type's move constructor is declared noexcept or when no usable copy constructor exists, and const T& otherwise. The compiler settles this; the returned reference then picks the constructor by ordinary overload resolution.

How it is written: identically to std::move, one argument, and it slots into any position that cast occupies.

The asymmetry it protects against: a copy leaves its source untouched, so a failure costs only the unfinished destination. A move writes to its source in order to be cheap, so a failure partway can leave both objects holding nothing complete, and undoing it would require the very operation that just failed.

What that looks like: relocating three objects with two slots available emptied the first two sources before the third threw. The same relocation through std::move_if_noexcept copied instead, failed at the same point, and left all three sources intact.

The exception to the rule: a move-only type with a potentially-throwing move constructor gets moved anyway. There is nothing else the cast can do, and no warning is issued.

Why it concerns you even if you never call it: container reallocation is built on it, so a missing noexcept on your move constructor turns every growth of a std::vector holding your type into a full round of copies.