jbab jbab - 4 years ago 83
C++ Question

What is the value of is_nothrow_copy_assignable for a class with a throwing copy constructor and a noexcept by-value copy assignment?

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.

Answer Source

The definition of is_nothrow_copy_assignable is in [meta.unary.prop]:

For a referenceable type T, the same result as is_nothrow_assignable_v<T&, const T&>, otherwise false.

Ok, A is referenceable (meaning A& is valid). So we go into is_nothrow_assignable:

is_assignable_v<T, U> is true 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 the noexcept operator is true if the set of potential exceptions of the expression (15.4) is empty, and false otherwise.

And in [except.spec]:

The exception-specification noexcept or noexcept(constant-expression), where the constant-expression yields true, denotes an exception specification that is the empty set. The exception-specification noexcept(constant-expression), where the constant-expression yields false, 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:

  1. invoking the copy constructor of A
  2. invoking the copy assignment operator of 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.

Recommended from our users: Dynamic Network Monitoring from WhatsUp Gold from IPSwitch. Free Download