vendredi 3 février 2017

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::boolalpha << 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 we can rely on std::is_nothrow_copy_assignable<A> and std::is_nothrow_move_assignable<A> providing the correct values (false and true, respectively). Otherwise, our class will behave badly, for example when used as the value type of a std::vector.

Aucun commentaire:

Enregistrer un commentaire