为什么std::exchange不是noexcept的?

3
根据标准 (N4659, §23.2.4, [utility.exchange]),std::exchange 应该执行 std::movestd::forward

template <class T, class U = T> T exchange(T& obj, U&& new_val);

Effects: Equivalent to:

T old_val = std::move(obj);
obj = std::forward<U>(new_val);
return old_val;

moveforward都标记为noexcept:
(N4659, §23.2.5, [forward]):

template <class T> constexpr T&& forward(remove_reference_t<T>& t) noexcept; template <class T> constexpr T&& forward(remove_reference_t<T>&& t) noexcept;

返回值: static_cast<T&&>(t)

(...)

template <class T> constexpr remove_reference_t<T>&& move(T&& t) noexcept;

返回值: static_cast<remove_reference_t<T>&&>(t)

那么,为什么exchange不是noexcept的呢? 这是否有其他原因还是委员会简单地忽略了这一点? 这是否正在被提出或者我可以自己写一个呢?


T(T&&) 可能不是 noexcept - Jarod42
4个回答

3

与默认仅依赖移动构造函数因此通常应该是noexceptstd::swap不同,如果新值需要复制,则std::exchange可能分配资源。虽然当new_valU&&且既不是移动赋值也不是旧值的移动抛出时,可能会有一个复杂的条件noexcept表达式,但似乎没有人提出这样的建议。


2

这段代码还使用了类型 T 的构造函数和赋值运算符。其中一个可能会抛出异常。


1
哦,你说得对。我不知道我怎么会忽略这个 :). 那么它的noexcept说明符中应该使用is_nothrow_move_assignable<T>is_nothrow_move_constructible<T>,对吧? - Elias Kosunen
1
这可能会相当复杂,因为它(可选地)涉及到一个不同的类型 U,它可能可以转换为 T。或者 T 可能有一个从 U 转换的构造函数。 - Bo Persson

0

C++23通过为std::exchange添加一个条件noexcept规范来解决这个问题 - 参见P2401R0


0
如果您想使用非标准版本来创建仅基于rvalues有条件地noexceptmove ctors,并且带有您使用的值,则可以使用此版本:
namespace estd {

template<class T, class U = T, std::enable_if_t<
        std::is_move_constructible<std::remove_reference_t<T>>::value && 
        std::is_assignable<std::remove_reference_t<T>, std::remove_reference_t<U>&&>::value && 
        !std::is_lvalue_reference<U>::value,
    int> = 0    
>
T rval_exchange(T& obj, U&& new_value) noexcept(
    std::is_nothrow_move_constructible<std::remove_reference_t<T>>::value &&
    std::is_nothrow_assignable<std::remove_reference_t<T>, std::remove_reference_t<U>&&>::value
)
{
    T old_value {std::move(obj)};
    obj = std::move(new_value);
    return old_value;
}

} // namespace estd

如果您在obj中使用左值引用作为要移动的值(即new_value必须是绑定到右值的右值引用),或者使用不可移动构造的类型T,或者移动构造不能与该右值一起工作,则模板将被禁用。

这样,您可以定义noexcept move ctors。 此处演示。我之所以将函数限制为仅适用于rvalues,是因为通常情况下,复制并非noexcept,尽管并非总是如此,并且因为在move ctors中应该使用exchange,而使其不是noexcept会使这些ctors不是noexcept,这对于明显的原因不好。

void do_stuff() noexcept
{ /*...*/ }

class Sample
{
    std::string mBody;
public:
    Sample(const std::string& body = ""): mBody {body} {}

    Sample(const Sample& s): mBody {s.mBody} {
        printf("%s\n", __PRETTY_FUNCTION__); // noexcept
    }

    Sample& operator=(const Sample& s) { 
        mBody = s.mBody; 
        printf("%s\n", __PRETTY_FUNCTION__); // noexcept
        return *this;
    }

    Sample(Sample&& dying) noexcept(
        noexcept(do_stuff()) && 
        noexcept(estd::rval_exchange(dying.mBody, {}))
    ):
        mBody {estd::rval_exchange(dying.mBody, {})}
    {
        do_stuff(); // noexcept
        printf("%s\n", __PRETTY_FUNCTION__); // noexcept
    }

    Sample& operator=(Sample&& dying) noexcept(
        noexcept(do_stuff()) && 
        noexcept(estd::rval_exchange(dying.mBody, {}))
    )
    {
        mBody = estd::rval_exchange(dying.mBody, {});
        do_stuff();
        printf("%s\n", __PRETTY_FUNCTION__); // noexcept
        return *this;
    }

    std::string body() const noexcept {return mBody;}
};

int main()
{
    std::cout << std::boolalpha;
    Sample rval{"wow such string very content"};
    Sample dummy;
    std::cout << noexcept( Sample(std::move(rval)) ) << std::endl; // prints true
    std::cout << noexcept( dummy = std::move(rval) ) << std::endl; // prints true

    // The rest only to show that move semantics actually work
    Sample f (std::move(rval));             // Calls move ctor
    std::cout << rval.body() << std::endl;  // prints empty line, empty string moved in rval
    std::cout << f.body() << std::endl;     // prints wow such string very content

    // estd::rval_exchange(f, rval); // Fails to compile bc rval is an lvalue reference, template disabled
    std::cout << (estd::rval_exchange(f, std::move(rval))).body() << std::endl; 
    // Ok, calls move ctor and move assignment (in estd::rval_exchange) and 
    // prints wow such string very content
    std::cout << f.body() << std::endl; // empty line, rval (empty) moved in f
    std::cout << "end" << std::endl;
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接