Расширяя некоторый ранее существующий код, я столкнулся с ситуацией, включающей несколько вложенных классов и конструкцию move, которая вызвала очень неожиданное поведение. В конечном итоге мне удалось создать два возможных исправления, но я не уверен, что с самого начала полностью понимаю проблему.
Вот несколько минимальный пример, в котором класс Foo
содержит поле типа SubFoo
и уникальный указатель и имеет различные конструкторы копирования и перемещения для отражения принадлежности уникального указателя. Обратите внимание, что есть три макроса, которые не определены - соответствуют исходному рабочему состоянию кода (т.е. ни одно из утверждений не выполнено).
#include <iostream>
#include <unordered_map>
#include <memory>
#include <vector>
#include <cassert>
//#define ADDMAP
//#define SUBFOO_MOVE
//#define FOO_MOVE_NONDEFAULT
class SubFoo {
public:
SubFoo() {}
SubFoo(const SubFoo& rhs) = default;
#ifdef SUBFOO_MOVE
SubFoo(SubFoo&& rhs) noexcept = default;
#endif
private:
#ifdef ADDMAP
std::unordered_map<uint32_t,uint32_t> _map;
#endif
};
class Foo {
public:
Foo(const std::string& name, uint32_t data)
: _name(name),
_data(std::make_unique<uint32_t>(std::move(data))),
_sub()
{
}
Foo(const Foo& rhs)
: _name(rhs._name),
_data(nullptr),
_sub(rhs._sub)
{
std::cout << "\tCopying object " << rhs._name << std::endl;
}
#ifdef FOO_MOVE_NONDEFAULT
Foo(Foo&& rhs) noexcept
: _name(std::move(rhs._name)),
_data(std::move(rhs._data)),
_sub(std::move(rhs._sub))
{
std::cout << "\tMoving object " << rhs._name << std::endl;
}
#else
Foo(Foo&& rhs) noexcept = default;
#endif
std::string _name;
std::unique_ptr<uint32_t> _data;
SubFoo _sub;
};
using namespace std;
int main(int,char**) {
std::vector<Foo> vec;
/* Add elements to vector so that it has to resize/reallocate */
cout << "ADDING PHASE" << endl;
for (uint i = 0; i < 10; ++i) {
std::cout << "Adding object " << i << std::endl;
vec.emplace_back(std::to_string(i),i);
}
cout << endl;
cout << "CHECKING DATA..." << endl;
for (uint i = 0; i < vec.size(); ++i) {
const Foo& f = vec[i];
assert(!(f._data.get() == nullptr || *f._data != i));
}
}
Как уже упоминалось выше, это рабочее состояние кода: поскольку элементы добавляются в вектор и он должен быть перераспределен в памяти, вызывается конструктор перемещения по умолчанию, а не конструктор копирования, о чем свидетельствует тот факт, что «Копирование объекта» # "никогда не печатается и поля уникального указателя остаются действительными.
Однако после добавления неупорядоченного поля карты к SubFoo
(которое в моем случае не было полностью пустым, а содержало только несколько базовых типов), конструктор перемещения больше не используется при изменении размера / перераспределении вектора. Вот ссылка coliru , где вы можете запустить этот код, для которого включен макрос ADDMAP
, что приводит к ошибочным утверждениям, поскольку конструктор копирования вызывается при изменении размера вектора, а уникальные указатели становятся недействительными.
В конце концов я нашел два решения:
- Добавление конструктора перемещения по умолчанию для
SubFoo
- Использование конструктора перемещения не по умолчанию для
Foo
, который выглядит точно так же, как я бы предположил, сделал конструктор перемещения по умолчанию.
Вы можете попробовать их в колирусе, раскомментировав любой из
SUBFOO_MOVE
или FOO_MOVE_NONDEFAULT
макросов.
Однако, хотя у меня есть некоторые грубые догадки (см. Постскрипты), я в основном запутался и не очень понимаю, почему код был сломан в первую очередь, и почему ни одно из исправлений не исправило его. Может ли кто-нибудь дать хорошее объяснение тому, что здесь происходит?
P.S. Интересно, что, хотя я могу быть не в курсе, что если неупорядоченная карта в SubFoo
каким-то образом делает конструкцию перемещения из Foo
недоступной, почему компилятор не предупреждает, что конструктор перемещения = default
невозможно
P.P.S. Кроме того, хотя в показанном здесь коде я использовал везде, где это возможно, конструкторы перемещения «noexcept», у меня возникли некоторые разногласия по поводу того, возможно ли это. Например, clang предупредил меня, что для Foo(Foo&& rhs) noexcept = default
«ошибка: спецификация исключения явно заданного по умолчанию конструктора перемещения не соответствует вычисленному». Это связано с вышеизложенным? Возможно, конструктор перемещения, используемый при изменении размера вектора, не должен быть исключением, и каким-то образом мой не совсем ...
РЕДАКТИРОВАТЬ В ОТНОШЕНИИ NOEXCEPT
Вероятно, здесь есть некоторая зависимость от компилятора, но для версии g ++, используемой coliru, конструктор перемещения (по умолчанию) для SubFoo
не не должен иметь noexcept, указанный для решения проблемы изменения размера вектора (которая это не то же самое, что указание noexcept(false)
, что не работает):
non-noexcept SubFoo move ctor работает
в то время как пользовательский конструктор перемещения для Foo
должен не быть исключением для исправления:
non-noexcept Foo Move Ctor не работает