В моей программе есть большое количество крупных объектов. В настоящее время они хранятся в std::set
с функтором сравнения клиентов. Набор изначально пуст, и я продолжаю помещать в него объекты. Еще я периодически «съедаю» предметы с одного конца набора.
В нескольких (2-10) ключевых моментах во время выполнения моей программы я мог бы захотеть изменить сортировку этих объектов и продолжать размещать и использовать их в новом порядке. Обычно у меня есть три возможных порядка, и я постоянно переключаюсь между ними.
Мне бы хотелось, чтобы прикатывание происходило не копируя крупные объекты, а перемещая их.
Рассмотрим следующий пример:
#include <iostream>
#include <set>
#include <utility>
struct S {
int a;
S(int a) : a{a} { std::cout << "Constructor\n"; }
S(const S& s) : a{s.a} { std::cout << "Copy constructor\n"; }
S(S&& s) : a{std::exchange(s.a, 0)} { std::cout << "Move constructor\n"; }
S& operator=(const S& s) { std::cout << "Copy assignment\n"; return *this = S(s); }
S& operator=(S&& s) { std::cout << "Move assignment\n"; std::swap(a, s.a); return *this; }
};
int main() {
auto order = [] (const S& s1, const S& s2) -> bool { return s1.a < s2.a; };
auto inver = [] (const S& s1, const S& s2) -> bool { return s2.a < s1.a; };
std::set<S, decltype(order)> set1;
set1.emplace(1);
set1.emplace(3);
set1.emplace(2);
for(auto&& s : set1) {
std::cout << s.a << " ";
}
std::cout << "\n";
std::set<S, decltype(inver)> set2{set1.begin(), set1.end()};
for(auto&& s : set2) {
std::cout << s.a << " ";
}
std::cout << "\n";
return 0;
}
Который печатает следующий вывод:
Constructor
Constructor
Constructor
1 2 3
Copy constructor
Copy constructor
Copy constructor
3 2 1
Можно ли использовать конструктор перемещения вместо конструктора копирования? Как? Если это невозможно, какая стратегия будет хорошей для решения моей проблемы? Должен ли я просто хранить свои объекты в структуре данных, которая не делает указатели недействительными и имеет прямой доступ в постоянное время (например, большой вектор, размер которого никогда не изменяется - или же, пожалуйста, предложите более подходящую структуру) и сортировать только их индексы?
Похоже, вы могли бы использовать set::extract
для удаления элементов из первого набора и перегрузку дескриптора узла set::insert
, чтобы добавить их во второй набор. Нет S
не будет скопировано или перемещено во время этого процесса.
Так
#include <set>
#include <iostream>
#include <utility>
struct S {
int a;
S(int a) : a{a} { std::cout << "Constructor\n"; }
S(const S& s) : a{s.a} { std::cout << "Copy constructor\n"; }
S(S&& s) : a{std::exchange(s.a, 0)} { std::cout << "Move constructor\n"; }
S& operator=(const S& s) { std::cout << "Copy assignment\n"; return *this = S(s); }
S& operator=(S&& s) { std::cout << "Move assignment\n"; std::swap(a, s.a); return *this; }
};
int main() {
auto order = [] (const S& s1, const S& s2) -> bool { return s1.a < s2.a; };
auto inver = [] (const S& s1, const S& s2) -> bool { return s2.a < s1.a; };
std::set<S, decltype(order)> set1;
set1.emplace(1);
set1.emplace(3);
set1.emplace(2);
std::set<S, decltype(inver)> set2;
// extract the first node
auto h = set1.extract(set1.begin());
// add to the second set
set2.insert(std::move(h));
for(auto&& s : set1) {
std::cout << s.a << " ";
}
std::cout << "\n";
for(auto&& s : set2) {
std::cout << s.a << " ";
}
std::cout << "\n";
return 0;
}
Выход
Constructor
Constructor
Constructor
2 3
1
Как видите, ничего не копировалось и не перемещалось.
Вы можете использовать std::set::extract (с std::move
и std::set::emplace) следующим образом:
std::set<S, decltype(inver)> set2;
auto it = set1.begin();
while (it != set1.end())
{
auto tmp = it;
it++; // increment `it` while it is still valid
set2.emplace(std::move(set1.extract(tmp).value())); // this will invalidate `tmp`, but we not longer need it for the loop
}
Обратите внимание, что мы используем итератор tmp
для extract
(который делает его недействительным) при увеличении it
, который используется в цикле заранее, пока он все еще действителен.
Таким образом, вместо конструктора копирования будет вызываться конструктор перемещения S
.
Другой вариант — использовать std::set::insert с дескриптором узла (вместо std::set::emplace).
Это переместит весь узел, и даже конструктор перемещения S
не будет вызван:
auto it = set1.begin();
while (it != set1.end())
{
auto tmp = it;
it++; // increment `it` while it is still valid
set2.insert(std::move(set1.extract(tmp))); // this will invalidate `tmp`, but we not longer need it for the loop
}
Звучит здорово! Избегаю ли я также признания итераторов недействительными, если использую следующий код? for(auto it = set1.begin(); it != set1.end();) {
set2.insert(std::move(set1.extract(it++)));
}
@AlbertoSantini, это немного сложно. Я не уверен насчет порядка оценки, поэтому на всякий случай предпочел использовать tmp
. Но, как я уже сказал, я не уверен.
@wohlstad Мне интересно, почему вы вызвали value()
в дескрипторе узла, не проще ли и эффективнее просто передать дескриптор узла в insert
?
@Джон, я думаю, ты прав. Я был ориентирован на вызов конструктора перемещения, как первоначально просил ОП. Использование вставки с дескриптором узла позволит избежать даже вызова конструктора перемещения. Я добавлю это к своему ответу.
Спасибо Джону и Вольстаду за ответы. Я принимаю ответ Вольстада, потому что он содержит пример, в котором все элементы переносятся из одного набора в другой.