Недавно я понял, что добавление семантики перемещения в 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
. Однако я не знаю, каковы правила аннулирования итератора для назначения перемещения, поэтому, если вы знаете о них, возможно, стоит упомянуть их в своем ответе.
2 ответа
C ++ 11
Различие в поведении OP между C ++ 03 и C ++ 11 связано с тем, как реализовано назначение перемещения. Есть два основных варианта:
Уничтожьте все элементы LHS. Освободите основное хранилище LHS. Переместите нижележащий буфер (указатели) с правой на левую.
Переместить-назначить от элементов 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 запрещено перемещать отдельные элементы.
Мой вопрос: является ли такое поведение ошибкой или оно преднамеренное? Это вообще хоть как-то предусмотрено стандартом?
Нет, да, нет (возможно).
См. этот ответ для подробного описания того, как 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();
Последнее - то, что я рекомендую. Первый (имхо) запутан.
Похожие вопросы
Связанные вопросы
Новые вопросы
c++
C++ — это язык программирования общего назначения. Изначально он разрабатывался как расширение C и имел аналогичный синтаксис, но теперь это совершенно другой язык. Используйте этот тег для вопросов о коде, который будет скомпилирован с помощью компилятора C++. Используйте тег версии для вопросов, связанных с конкретной стандартной версией [C++11], [C++14], [C++17], [C++20] или [C++23]. и т.д.