Недавно я понял, что добавление семантики перемещения в C ++ 11 (или, по крайней мере, моя реализация ее, Visual C ++) активно (и довольно резко) сломало одну из моих оптимизаций.

Рассмотрим следующий код:

#include <vector>
int main()
{
    typedef std::vector<std::vector<int> > LookupTable;
    LookupTable values(100);  // make a new table
    values[0].push_back(1);   // populate some entries

    // Now clear the table but keep its buffers allocated for later use
    values = LookupTable(values.size());

    return values[0].capacity();
}

Я следовал этому шаблону для выполнения повторного использования контейнера : я бы повторно использовал один и тот же контейнер вместо его уничтожения и воссоздания, чтобы избежать ненужного освобождения кучи и (немедленного) перераспределения.

В C ++ 03 это работало нормально - это означает, что этот код использовался для возврата 1, потому что векторы были скопированы поэлементно, а их базовые буферы оставались как есть. Следовательно, я мог изменять каждый внутренний вектор, зная, что он может использовать тот же буфер, что и раньше.

Однако в C ++ 11 я заметил, что это приводит к перемещению правой части на левую, что выполняет поэлементное присвоение перемещения каждому вектору на левая сторона. Это, в свою очередь, приводит к тому, что вектор отбрасывает свой старый буфер, внезапно уменьшая его емкость до нуля. Следовательно, мое приложение теперь значительно замедляется из-за избыточного выделения / освобождения кучи.

Мой вопрос: является ли такое поведение ошибкой или оно преднамеренное? Это вообще хоть как-то предусмотрено стандартом?

Обновить:

Я только что понял, что правильность этого конкретного поведения может зависеть от того, может ли a = A() аннулировать итераторы, указывающие на элементы a. Однако я не знаю, каковы правила аннулирования итератора для назначения перемещения, поэтому, если вы знаете о них, возможно, стоит упомянуть их в своем ответе.

22
user541686 3 Авг 2014 в 15:03

2 ответа

Лучший ответ

C ++ 11

Различие в поведении OP между C ++ 03 и C ++ 11 связано с тем, как реализовано назначение перемещения. Есть два основных варианта:

  1. Уничтожьте все элементы LHS. Освободите основное хранилище LHS. Переместите нижележащий буфер (указатели) с правой на левую.

  2. Переместить-назначить от элементов RHS к элементам LHS. Уничтожьте любые лишние элементы в LHS или переместите и создайте новые элементы в LHS, если в RHS их больше.

Я думаю, что можно использовать вариант 2 с копиями, если перемещение не исключено.

Вариант 1 делает недействительными все ссылки / указатели / итераторы на LHS и сохраняет все итераторы и т. Д. В RHS. Требуется разрушение O(LHS.size()), но само перемещение буфера равно O (1).

Вариант 2 делает недействительными только итераторы для лишних элементов LHS, которые уничтожаются, или все итераторы, если происходит перераспределение LHS. Это O(LHS.size() + RHS.size()), поскольку необходимо позаботиться обо всех элементах обеих сторон (скопировать или уничтожить).

Насколько я могу судить, нет никакой гарантии, что именно произойдет в C ++ 11 (см. Следующий раздел).

Теоретически вы можете использовать вариант 1 всякий раз, когда вы можете освободить нижележащий буфер с помощью распределителя, который сохраняется в LHS после операции. Этого можно добиться двумя способами:

  • Если два распределителя сравниваются одинаково, один может использоваться для освобождения памяти, выделенной другим. Следовательно, если перед перемещением распределители LHS и RHS сравнивают равные, вы можете использовать вариант 1. Это решение во время выполнения.

  • Если распределитель может быть распространен (перемещен или скопирован) из RHS в LHS, этот новый распределитель в LHS можно использовать для освобождения памяти RHS. Распространяется ли распределитель или нет, определяется allocator_traits<your_allocator :: propagate_on_container_move_assignment. Это определяется свойствами типа, то есть решением времени компиляции.


C ++ 11 минус дефекты / C ++ 1y

После LWG 2321 (который все еще open) мы гарантируем, что:

нет конструктора перемещения (или оператора присваивания перемещения, когда allocator_traits<allocator_type> :: propagate_on_container_move_assignment :: value является true) контейнера (кроме массива) делает недействительными любые ссылки, указатели или итераторы, относящиеся к элементам источника контейнер. [ Примечание . Итератор end() не ссылается ни на один элемент, поэтому он может быть признан недействительным. - конец примечания ]

Это требует , чтобы назначение перемещения для тех распределителей, которые распространяются при назначении перемещения, должно перемещать указатели объекта vector, но не должно перемещать элементы вектора. (Опция 1)

Распределитель по умолчанию после дефекта 2103 LWG , распространяется во время присвоения контейнера перемещению, поэтому трюк в OP запрещено перемещать отдельные элементы.


Мой вопрос: является ли такое поведение ошибкой или оно преднамеренное? Это вообще хоть как-то предусмотрено стандартом?

Нет, да, нет (возможно).

18
dyp 3 Авг 2014 в 22:30

См. этот ответ для подробного описания того, как vector переместить назначение должно работать. Поскольку вы используете std::allocator, C ++ 11 помещает вас в случай 2, который многие в комитете сочли дефектом и который был исправлен на случай 1 для C ++ 14.

И случай 1, и случай 2 имеют идентичное поведение во время выполнения, но случай 2 имеет дополнительные требования времени компиляции для vector::value_type. Оба случая 1 и случай 2 приводят к тому, что владение памятью передается от правого к левому во время присвоения перемещения, что дает вам наблюдаемые вами результаты.

Это не ошибка. Это сделано намеренно. Он указан в C ++ 11 и последующих версиях. Да, есть некоторые мелкие недоработки, как указал dyp в своем ответе. Но ни один из этих дефектов не изменит наблюдаемого вами поведения.

Как было указано в комментариях, самое простое решение для вас - создать помощник as_lvalue и использовать его:

template <class T>
constexpr
inline
T const&
as_lvalue(T&& t)
{
    return t;
}

// ...

// Now clear the table but keep its buffers allocated for later use
values = as_lvalue(LookupTable(values.size()));

Это не требует затрат и возвращает вас точно к поведению C ++ 03. Однако он может не пройти проверку кода. Вам было бы проще перебирать и clear каждый элемент во внешнем векторе:

// Now clear the table but keep its buffers allocated for later use
for (auto& v : values)
    v.clear();

Последнее - то, что я рекомендую. Первый (имхо) запутан.

11
Community 23 Май 2017 в 10:33