简体   繁体   中英

Why does code with std::vector not compile but with std::unique_ptr it does, if there is no noexcept move constructor?

Why can the following program not be compiled?

NB: something_t's move constructor is not noexcept.

#include <memory>
#include <vector>

class something_t {
public:
    constexpr something_t() = default;

    constexpr something_t(const something_t& other)
        : field_(other.field_) {
    }

    constexpr something_t(something_t&& other)
        : field_(other.field_) {
    }

private:
    unsigned int field_{ 0 };
};

struct data_t {
    something_t something;
    std::vector<std::unique_ptr<int>> move_only; // <-- this line
};

int main() {
    std::vector<data_t> result;
    data_t data;
    result.push_back(std::move(data));
    return 0;
}

Error is (within g++):

/usr/include/c++/9/bits/stl_uninitialized.h:127:72: error: static assertion failed: result type must be constructible from value type of input range
127 |       static_assert(is_constructible<_ValueType2, decltype(*__first)>::value,
    |                                                                        ^~~~~        

(nearly the same with clang and MSVC).

If I replace the line with the "this line" comment by std::unique_ptr<int> move_only then the code compiles fine:

    struct data_t {
        something_t something;
        std::unique_ptr<int> move_only;
    };

Why does removing std::vector help? It also compiles with or without std::vector if I make the something_t move constructor noexcept.

NB: adding noexcept to something_t 's move constructor helps, but that's not the question.

Question is:

Why with this:

    struct data_t {
        something_t something;
        std::unique_ptr<int> move_only;
    };

does the program compile?

But with

struct data_t {
    something_t something;
    std::vector<std::unique_ptr<int>> move_only; // <-- this line
};

does the program NOT compile?

In fact, both std::unique_ptr<int> and std::vector<std::unique_ptr<int>> :

  • non-copyable
  • noexcept-moveable

So they have the same properties.

Update: I've tried to compare the type_traits of both variants:

                                                data_t(vector)          data_t(unique_ptr):
is_constructible:                               true                    true
is_trivially_constructible:                     false                   false
is_nothrow_constructible:                       true                    true
is_default_constructible:                       true                    true
is_trivially_default_constructible:             false                   false
is_nothrow_default_constructible:               true                    true
is_copy_constructible:                          true                    false
is_trivially_copy_constructible:                false                   false
is_nothrow_copy_constructible:                  false                   false
is_move_constructible:                          true                    true
is_trivially_move_constructible:                false                   false
is_nothrow_move_constructible:                  false                   false
is_assignable:                                  false                   false
is_trivially_assignable:                        false                   false
is_nothrow_assignable:                          false                   false
is_copy_assignable:                             false                   false
is_trivially_copy_assignable:                   false                   false
is_nothrow_copy_assignable:                     false                   false
is_move_assignable:                             false                   false
is_trivially_move_assignable:                   false                   false
is_nothrow_move_assignable:                     false                   false
is_destructible:                                true                    true
is_trivially_destructible:                      false                   false
is_nothrow_destructible:                        true                    true
is_swappable:                                   false                   false
is_nothrow_swappable:                           false                   false

The only difference is:

is_copy_constructible:                          true                    false

Ie, data_t with vector is copy-constructible, and with unique_ptr it is not. But how can this difference affect compilation?

The important difference here is:

std::is_copy_constructible<std::vector<std::unique_ptr<int>>>::value == true
std::is_copy_constructible<std::unique_ptr<int>>::value == false

That first one is maybe surprising. But note that is_copy_constructible and most similar type traits only require that the operation they test is declared, not that it would be valid to actually use. std::vector unfortunately lacks some "SFINAE correctness" here, but that might be intentional for backwards compatibility.

The Standard's description of template <class T, class Allocator> class vector in [vector.overview]/2 simply says that it declares a member vector(const vector& x); . The following sections say nothing else about the copy constructor. In particular, std::vector doesn't have a piece similar to this sentence from [optional.ctor]/6 about the copy constructor of std::optional<T> :

 constexpr optional(const optional& rhs);

Remarks: This constructor shall be defined as deleted unless is_copy_constructible_v<T> is true.

Because of the various requirements on std::vector<T> , its functions like push_back , insert , and emplace which need to deal with the possibility of reallocating and populating new memory with elements already in the vector are forced to be implemented like this:

  • If std::is_nothrow_move_constructible<T>::value is true, uses the move constructor of T , and the functions provide the strong exception guarantee.
  • If std::is_nothrow_move_constructible<T>::value is false and std::is_copy_constructible<T>::value is true, uses the copy constructor of T , and the functions provide the strong exception guarantee.
  • If std::is_nothrow_move_constructible<T>::value and std::is_copy_constructible<T>::value are both false, uses the move constructor of T , but the functions cannot provide the strong exception guarantee.

( T must be move constructible, which might actually mean using a copy constructor, as a general requirement of these container functions.)

So when data_t has a std::vector<std::unique_ptr<int>> member, it "incorrectly" has an implicitly declared copy constructor which is not deleted. This leads to std::vector<data_t>::push_back choosing the second option from the list above, but the actual use of the copy constructor leads to errors.

When data_t has a std::unique_ptr<int> member, its deleted copy constructor means that the implicitly declared copy constructor of data_t is also deleted. So in this case, std::vector<data_t>::push_back chooses the third option from the list above, using the move constructor, but if it does throw, the vector is left in an unspecified state.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM