What is, in terms of the C++ standard, the expected (if any) output of the following program:
#include <iostream>
#include <iomanip>
#include <type_traits>
class A {
public:
A() = default;
~A() = default;
A(A const& other) {}
A(A&& other) noexcept {}
A& operator=(A other) noexcept { return *this; }
};
int main() {
std::cout << std::boolalpha
<< std::is_nothrow_copy_assignable<A>::value << "\n"
<< std::is_nothrow_move_assignable<A>::value << "\n";
}
In other words, does the evaluation of the type traits' values look at the declaration of the assignment operator only, which is noexcept, and does it thus yield
true
true
Or does it consider the calling context (a
, b
are instances of A
)
a = b; // may throw, implicitly calls copy c'tor
a = std::move(b); // noexcept, implicitly calls move c'tor
and does it yield
false
true
Practical attempts
Running the code with Visual Studio 2015, Update 3 gives
true
true
whereas gcc 6.1 gives
false
true
Who is right?
Background
A situation like this occurs when we have a resource managing class with a throwing copy constructor (since resource allocation may fail), a noexcept move constructor, a throwing copy assignment and a noexcept move assignment.
Supposing that both copy and move assignment can be efficiently implemented in terms of the swap idom:
A& operator=(A const& other) {
A(other).swap(*this); // calls the copy c'tor, may throw
return *this;
}
A& operator=(A&& other) noexcept {
A(std::move(other)).swap(*this); // calls noexcept move c'tor
return *this;
}
Then we might consider condensing both into the single by-value copy assignment
A& operator=(A other) noexcept {
other.swap(*this);
return *this;
}
However, we can only safely do this if std::is_nothrow_copy_assignable<A>
and std::is_nothrow_move_assignable<A>
provide the correct values (false and true, respectively). Otherwise, code relying on these type traits would behave badly and our single by-value assignment would not be a correct replacement for two separate assignment operators.
The definition of is_nothrow_copy_assignable
is in [meta.unary.prop]:
For a referenceable type
T
, the same result asis_nothrow_assignable_v<T&, const T&>
, otherwisefalse
.
Ok, A
is referenceable (meaning A&
is valid). So we go into is_nothrow_assignable
:
is_assignable_v<T, U>
istrue
and the assignment is known not to throw any exceptions (5.3.7).
is_assignable_v<A, A const&>
is definitely true
, so we satisfy the first part. What does it mean to be known not to throw any exceptions? According to [expr.unary.noexcept]:
The
noexcept
operator determines whether the evaluation of its operand, which is an unevaluated operand (Clause 5), can throw an exception (15.1). [...] The result of thenoexcept
operator istrue
if the set of potential exceptions of the expression (15.4) is empty, and false otherwise.
And in [except.spec]:
The exception-specification
noexcept
ornoexcept(constant-expression)
, where the constant-expression yieldstrue
, denotes an exception specification that is the empty set. The exception-specificationnoexcept(constant-expression)
, where the constant-expression yieldsfalse
, or the absence of an exception-specification in a function declarator other than that for a destructor (12.4) or a deallocation function (3.7.4.2) denotes an exception specification that is the set of all types.
And:
The set of potential exceptions of an expression e is empty if e is a core constant expression (5.20). Otherwise, it is the union of the sets of potential exceptions of the immediate sub-expressions of e, including default argument expressions used in a function call, combined with a set S defined by the form of e, as follows: [...]
— If e implicitly invokes one or more functions (such as an overloaded operator, an allocation function in a new-expression, or a destructor if e is a full-expression (1.9)), S is the union of:
— the sets of types in the exception specifications of all such functions, and
— if e is a new-expression [...]
Now, the assignment of an A
from an A const&
involves two steps:
A
A
The exception specification is the union of all the exception specifications of both of these functions, which is the set of all types - because the copy constructor does not have an exception-specification at all.
Therefore, is_nothrow_copy_assignable_v<A>
should be false
. gcc is correct.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With