4

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

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

Boann
  • 48,794
  • 16
  • 117
  • 146
vladon
  • 8,158
  • 2
  • 47
  • 91
  • it seems your question is really why the code without `move_only` CAN be compiled? The problem with unique_ptr has been answered several times the last couple of days – M.M May 31 '20 at 01:16
  • my question is: why with `std::vector>` it cannot be compiled, but with `std::unique_ptr` it CAN be compiled? – vladon May 31 '20 at 01:19
  • @M.M The question is, why this code with `std::unique_ptr move_only;` does compile. It seems `std::vector>` has the same properties as `std::unique_ptr` where it matters (namely, both are not copyable, and both are noexcept-movable). I, for one, am baffled that replacing one with the other makes a difference. – Igor Tandetnik May 31 '20 at 01:20
  • @IgorTandetnik Yes – vladon May 31 '20 at 01:20
  • Are you saying that adding `noexcept` to `something_t` move ctor makes it compile with and without `std::vector`? – Slava May 31 '20 at 01:22
  • @Slava Yes, after adding noexcept the program compiles successfully. – vladon May 31 '20 at 01:23
  • @Slava Yes, but [that's easy to explain](https://stackoverflow.com/questions/8001823/how-to-enforce-move-semantics-when-a-vector-grows). That's not the surprising part. – Igor Tandetnik May 31 '20 at 01:23
  • @IgorTandetnik Probably, it is just not clear from OP's explanation. If you clearly understand what OP means you probably should correct the question, it is quite cryptic even for me. – Slava May 31 '20 at 01:24
  • @Slava I've tried to clarify my question, please take a look. – vladon May 31 '20 at 01:29
  • Its ok, just this "There is the same problem with absence with noexcept, isn't it?" is very confusing. I can try to fix it but English is not my first language either. Btw what NB means? – Slava May 31 '20 at 01:33
  • NB == nota bene == take a look – vladon May 31 '20 at 01:35
  • 1
    I tried to fix it as much as I can, if you do not mind – Slava May 31 '20 at 01:45
  • @IgorTandetnik I read the link, looks like they are doing their best to kill the language. Quite soon it will be impossible to understand what is going on. I could only imagine how difficult to novice programmer to write correct and efficient programs. – Slava May 31 '20 at 01:55
  • @IgorTandetnik @Slava I've tried to compare type_traits for both variants of `data_t`, the only difference is that `data_t` with `vector` is copy constructible, and with `unique_ptr` it is not. I've updated the question with comparison table. But I still cannot understand how this affects compilation. – vladon May 31 '20 at 02:07
  • Asked another question about this: https://stackoverflow.com/questions/62110834/why-is-copy-constructible-vvectorunique-ptrint-is-true-but-it-cannot-be-c – vladon May 31 '20 at 02:21

1 Answers1

5

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.

Community
  • 1
  • 1
aschepler
  • 70,891
  • 9
  • 107
  • 161
  • Thanks! So, is this a bug in STL (or Standard)? – vladon May 31 '20 at 02:34
  • Bug, oversight, or backwards-compatibility feature. Thankfully the issue can be resolved by explicitly deleting the `data_t` copy constructor and copy-assignment operators. – cdhowie May 31 '20 at 02:36
  • See also https://groups.google.com/a/isocpp.org/forum/#!msg/std-proposals/pgWYGIvV2Tw/8OteiWGUXfwJ . Some suggested there that many things like this might be changed once concepts are in the language. Though I don't know if the committee would go for that, since it could change meaning of existing code. And the C++2a draft has concepts, but has not changed the vector copy constructor or other such things. – aschepler May 31 '20 at 02:50