Наличие unordered_map определяет, используется ли конструктор копирования или конструктор перемещения - PullRequest
0 голосов
/ 09 марта 2019

Расширяя некоторый ранее существующий код, я столкнулся с ситуацией, включающей несколько вложенных классов и конструкцию 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 не работает

1 Ответ

1 голос
/ 09 марта 2019

Существует стандартный дефект (на мой взгляд), что ctor неупорядоченной карты не исключение.

Таким образом, дефолтный ход ctor, являющийся noexcept (false) или удаленный вашей попыткой по умолчанию noexcept (true), кажется правдоподобным.

Изменение размера вектора требует noexecept (true) ctor перемещения, потому что он не может разумно и эффективно восстанавливаться после броска хода 372-го элемента; он не может ни откатиться, ни продолжать идти. Это должно было бы остановиться с кучей элементов, пропавших как-то.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...