为什么 std::exchange 不是 noexcept?
Why is std::exchange not noexcept?
根据标准(N4659,§23.2.4,[utility.exchange]),std::exchange
应该做一个 std::move
和一个 std::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;
move
和forward
都标记为noexcept
:
(N4659,§23.2.5,[转发]):
template <class T> constexpr T&& forward(remove_reference_t<T>& t) noexcept;
template <class T> constexpr T&& forward(remove_reference_t<T>&& t) noexcept;
Returns: static_cast<T&&>(t)
.
(...)
template <class T> constexpr remove_reference_t<T>&& move(T&& t) noexcept;
Returns: static_cast<remove_reference_t<T>&&>(t)
.
那么为什么 exchange
noexcept
不是呢?是否还有其他原因,或者委员会只是忽略了这一点?这是在提议还是我可以写一个?
该代码还使用了类型 T
的构造函数和赋值运算符。其中之一可能会抛出。
与 std::swap
不同,后者默认仅依赖移动构造函数,因此通常应为 noexcept
,std::exchange
可能会在需要复制新值时分配资源。虽然当 new_val
是 U&&
并且移动赋值和旧值的移动抛出都不是时,条件 noexcept
可能有一个复杂的表达式,但似乎没有人提出这样的建议
如果您想使用非标准版本创建 noexcept
仅使用右值有条件地 noexcept
使用您使用的值来移动 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 移动构造函数。 Demo here。我将函数限制为右值的原因是,通常情况下,复制不是 noexcept
,尽管情况并非总是如此,而且因为 exchange
应该在 move ctors 中使用,同时拥有它not 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;
}
C++23 通过向 std::exchange
添加条件 noexcept
规范来修复此问题 - 请参阅 P2401R0
根据标准(N4659,§23.2.4,[utility.exchange]),std::exchange
应该做一个 std::move
和一个 std::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;
move
和forward
都标记为noexcept
:
(N4659,§23.2.5,[转发]):
template <class T> constexpr T&& forward(remove_reference_t<T>& t) noexcept;
template <class T> constexpr T&& forward(remove_reference_t<T>&& t) noexcept;
Returns:
static_cast<T&&>(t)
.(...)
template <class T> constexpr remove_reference_t<T>&& move(T&& t) noexcept;
Returns:
static_cast<remove_reference_t<T>&&>(t)
.
那么为什么 exchange
noexcept
不是呢?是否还有其他原因,或者委员会只是忽略了这一点?这是在提议还是我可以写一个?
该代码还使用了类型 T
的构造函数和赋值运算符。其中之一可能会抛出。
与 std::swap
不同,后者默认仅依赖移动构造函数,因此通常应为 noexcept
,std::exchange
可能会在需要复制新值时分配资源。虽然当 new_val
是 U&&
并且移动赋值和旧值的移动抛出都不是时,条件 noexcept
可能有一个复杂的表达式,但似乎没有人提出这样的建议
如果您想使用非标准版本创建 noexcept
仅使用右值有条件地 noexcept
使用您使用的值来移动 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 移动构造函数。 Demo here。我将函数限制为右值的原因是,通常情况下,复制不是 noexcept
,尽管情况并非总是如此,而且因为 exchange
应该在 move ctors 中使用,同时拥有它not 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;
}
C++23 通过向 std::exchange
添加条件 noexcept
规范来修复此问题 - 请参阅 P2401R0