Почему невозможно назначить вектор объектов, которым не хватает оператора копирования?

У меня есть вектор структур, которые можно скопировать, но не назначать:

struct Struct
{
    inline Struct(const std::string& text, int n) : _text(text), _n(n) {}
    inline Struct(const Struct& other) : _text(other._text), _n(other._n) {}

    const std::string _text;
    const int _n;

    Struct& operator=(const Struct&) = delete;
};

Все работало нормально. Фактически, я мог бы даже передать std::vector<Struct> вокруг по значению в качестве возвращаемого значения функции. И все же это не удается:

std::vector<TextFragment> v1, v2;
v2 = v1;

Ошибка, конечно, такова:

error: C2280: 'Struct & Struct:: operator = (const Struct &)': попытка ссылки на удаленную функцию

Я не понимаю, почему он пытается это сделать. Это какая-то оптимизация, чтобы избежать перераспределения блока векторной памяти?..

Ответ 1

Это какая-то оптимизация, чтобы избежать перераспределения блока векторной памяти?..

Почти. Оптимизация позволяет избежать перераспределения всех блоков памяти в vector value_type. То есть, это глобальное предположение, что назначение может быть более эффективным, чем разрушение, за которым следует построение копии.

Например, рассмотрите назначение vector<string> для двух равных размеров vector s и пучка равного размера string в каждом месте в vector:

v2 = v1;

Вся эта операция должна выполняться memcpy каждый string. Никаких ассигнований вообще. Сокращение распределения/освобождения является одной из самых важных оптимизаций, которые существуют сегодня.

Однако для вашего Struct все не потеряно. То, что вы хотите сделать, - это указать vector, что вы не хотите назначать свои Struct s, но вместо этого уничтожаете их, а затем скопируйте их из v1. Синтаксис для этого:

v2.clear();
for (const auto& x : v1)
    v2.push_back(x);

Как отмечено в комментариях ниже, вы также можете скопировать конструкцию v1, а затем swap с копией. Вам либо нужно создать временную локальную копию v1, либо вам нужно использовать v1 в "lhs" члена swap:

std::vector<Struct>(v1).swap(v2);

Мне лично это трудно читать. В С++ 11/14 я предпочитаю эту альтернативу, которая включает назначение переноса:

v2 = std::vector<Struct>(v1);

Эти альтернативы легче на глаза. Но первая альтернатива, использующая clear() и push_back, будет наиболее эффективной в среднем. Это связано с тем, что первая альтернатива - это единственная возможность повторного использования capacity() в v2. Остальные два всегда воссоздают новый capacity() в копии v1 и выбрасывают v2 существующий capacity().

Ответ 2

A std::vector является контейнером, поддерживающим распределитель. Если мы посмотрим на спецификацию для этого (таблица 99), мы имеем a = t, где a является неконстантным lvalue, а t является значением lvalue или const rvalue и требует

T является CopyInsertable в X и CopyAssignable. post: a == t

Акцент на мине

Где t - value_type of X (контейнер). В нем также указывается, что операция является линейной. Поскольку t должен быть CopyAssignable и Struct не является CopyAssignable, от него не требуется работать.

Это означает, что операция присваивания будет выглядеть примерно так:

std::vector<T>& operator=(const std::vector<T>& rhs)
{
    // allocate enough room for data if needed
    for (std::size_t i = 0; i < rhs.size(); ++i)
        data[i] = rhs.data[i];
    return *this;
}

Ответ 3

Вы можете найти эту функцию информативной, из libС++. В частности, обратите внимание на вызов std::copy.

template <class _Tp, class _Allocator>
template <class _ForwardIterator>
typename enable_if
<
    __is_forward_iterator<_ForwardIterator>::value &&
    is_constructible<
       _Tp,
       typename iterator_traits<_ForwardIterator>::reference>::value,
    void
>::type
vector<_Tp, _Allocator>::assign(_ForwardIterator __first, _ForwardIterator __last)
{
    size_type __new_size = static_cast<size_type>(_VSTD::distance(__first, __last));
    if (__new_size <= capacity())
    {
        _ForwardIterator __mid = __last;
        bool __growing = false;
        if (__new_size > size())
        {
            __growing = true;
            __mid =  __first;
            _VSTD::advance(__mid, size());
        }
        pointer __m = _VSTD::copy(__first, __mid, this->__begin_);
        if (__growing)
            __construct_at_end(__mid, __last, __new_size - size());
        else
            this->__destruct_at_end(__m);
    }
    else
    {
        deallocate();
        allocate(__recommend(__new_size));
        __construct_at_end(__first, __last, __new_size);
    }
}

operator = просто вызывает assign(r.begin(), r.end()) после исправления распределителя, если они отличаются.