Почему мои элементы данных копируются, а не перемещаются? - PullRequest
1 голос
/ 20 февраля 2020

Я выполняю некоторые тесты по семантике перемещения, и поведение моего класса кажется мне странным.

Учитывая фиктивный класс VecOfInt:

class VecOfInt {
public:
    VecOfInt(size_t num) : m_size(num), m_data(new int[m_size]) {}
    ~VecOfInt() { delete[] m_data; }
    VecOfInt(VecOfInt const& other) : m_size(other.m_size),  m_data(new int[m_size]) {
        std::cout << "copy..." <<std::endl;
        std::copy(other.m_data, other.m_data + m_size, m_data);
    }
    VecOfInt(VecOfInt&& other) : m_size(other.m_size) {
        std::cout << "move..." << std::endl;
        m_data = other.m_data;
        other.m_data = nullptr;
    }
    VecOfInt& operator=(VecOfInt const& other) {
        std::cout << "copy assignment..." << std::endl;
        m_size = other.m_size;
        delete m_data;
        m_data = nullptr;
        m_data = new int[m_size];
        m_data = other.m_data;
        return *this;
    }
    VecOfInt& operator=(VecOfInt&& other) {
        std::cout << "move assignment..." << std::endl;
        m_size = other.m_size;
        m_data = other.m_data;
        other.m_data = nullptr;
        return *this;
    }
private:
    size_t m_size;
    int* m_data;
};
  1. OK CASE

    Когда я вставляю одно значение на место:

    int main() {
        std::vector<VecOfInt> v;
        v.push_back(10);
        return 0;
    }
    

    Затем это дает мне следующий вывод (что я думаю, хорошо) :

    move...

  2. странный случай

    Когда Я вставляю три разных значения на месте:

    int main() {
        std::vector<VecOfInt> v;
        v.push_back(10);
        v.push_back(20);
        v.push_back(30);
        return 0;
    }
    

    Затем вывод вызывает конструктор копирования 3 раза:

    move... move... copy... move... copy... copy...

Что мне здесь не хватает?

Ответы [ 5 ]

6 голосов
/ 20 февраля 2020

Конструкция перемещения и назначение перемещения не используются std::vector при перераспределении, если они не равны noexcept или если нет альтернатив для копирования. Вот ваш пример с добавлением noexcept:

class VecOfInt {
public:
    VecOfInt(size_t num) : m_size(num), m_data(new int[m_size]) {}
    ~VecOfInt() { delete[] m_data; }
    VecOfInt(VecOfInt const& other) : m_size(other.m_size),  m_data(new int[m_size]) {
        std::cout << "copy..." <<std::endl;
        std::copy(other.m_data, other.m_data + m_size, m_data);
    }
    VecOfInt(VecOfInt&& other) noexcept : m_size(other.m_size) {
        std::cout << "move..." << std::endl;
        m_data = other.m_data;
        other.m_data = nullptr;
    }
    VecOfInt& operator=(VecOfInt const& other) {
        std::cout << "copy assignment..." << std::endl;
        m_size = other.m_size;
        delete m_data;
        m_data = nullptr;
        m_data = new int[m_size];
        m_data = other.m_data;
        return *this;
    }
    VecOfInt& operator=(VecOfInt&& other) noexcept {
        std::cout << "move assignment..." << std::endl;
        m_size = other.m_size;
        m_data = other.m_data;
        other.m_data = nullptr;
        return *this;
    }
private:
    size_t m_size;
    int* m_data;
};

Пример live * output:

move...
move...
move...
move...
move...
move...

Это сделано для обеспечения безопасности исключений. Если изменить размер std::vector не удастся, он попытается оставить вектор таким, каким он был до попытки. Но если операция перемещения перебрасывает половину перераспределения, то не существует безопасного способа отменить уже выполненные шаги. Они очень хорошо могут также бросить. Самое безопасное решение - скопировать, если движение может привести к броску.

2 голосов
/ 20 февраля 2020

tl; dr : std::vector будет копировать вместо перемещения, если ваш конструктор перемещения не noexcept.

1. Речь идет не о членах, а о динамических c распределениях

Проблема не в том, что вы делаете с полями foo. Таким образом, ваш источник может быть просто:

class foo {
public:
    foo(size_t num) {}
    ~foo() = default
    foo(foo const& other)  {
        std::cout << "copy..." <<std::endl;
    }
    foo(foo&& other) {
        std::cout << "move..." << std::endl;
    }
    foo& operator=(foo const& other) {
        std::cout << "copy assignment..." << std::endl;
        return *this;
    }
    foo& operator=(foo&& other) {
        std::cout << "move assignment..." << std::endl;
        return *this;
    }
};

, и вы все равно получаете такое же поведение: попробуйте .

2. Ход, который вы видите, отвлекает

Теперь push_back() сначала создаст элемент - в данном случае foo; затем убедитесь, что в векторе есть место для него; тогда std::move() это на свое место. Итак, 3 из ваших ходов такого рода. Давайте попробуем использовать emplace_back() вместо этого, что создаст элемент вектора на его месте:

#include <vector>
#include <iostream>

struct foo { // same as above */ };

int main() {
    std::vector<foo> v;
    v.emplace_back(10);
    v.emplace_back(20);
    v.emplace_back(30);
    return 0;
}

Это даст нам:

copy 
copy 
copy 

попробуем . Так что ходы были просто отвлечением.

3. Копии происходят из-за изменения размера вектора

Ваш std::vector постепенно увеличивается при вставке элементов, что требует либо перемещения, либо копирования конструкций. Подробнее см. @ сообщение NutCracker .

4. Реальная проблема - исключения

Смотрите этот вопрос:

Как применить семантику перемещения при росте вектора?

std::vector не знает он может безопасно перемещать элементы при изменении размера - где «безопасно» означает «без исключений», поэтому он возвращается к копированию.

5. «Но мой копирующий ctor тоже может выдавать исключение!»

Я думаю, логическое обоснование состоит в том, что если вы получаете исключение при копировании меньшего буфера - вы все равно не коснулись его, так что, по крайней мере, ваш оригинальный, не -размерный вектор действителен и может быть использован. Если вы начали перемещение элементов и получили исключение - тогда у вас все равно нет действительной копии элемента, не говоря уже о действительном меньшем векторе.

2 голосов
/ 20 февраля 2020

std::vector выделяет блок непрерывной памяти для своих элементов. Когда выделенная память слишком коротка для хранения новых элементов, выделяется новый блок, и все текущие элементы копируются из старого блока в новый блок.

Вы можете использовать std::vector::reserve() для предварительного увеличения объема памяти std::vector перед добавлением новых элементов.

Попробуйте выполнить следующее:

int main() {
    std::vector<VecOfInt> v;
    v.reserve(3);
    v.push_back(10);
    v.push_back(20);
    v.push_back(30);
    return 0;
}

, и вы получите:

move...
move...
move...

Но чтобы вызывать конструктор перемещения даже при перераспределении, вы должны сделать его noexcept вроде:

VecOfInt(VecOfInt&& other) noexcept {...}
1 голос
/ 20 февраля 2020

Ваш конструктор перемещения не имеет спецификатора noexcept.

Объявите его как

VecOfInt(VecOfInt&& other) noexcept : m_size(other.m_size) {
    std::cout << "move..." << std::endl;
    m_data = other.m_data;
    other.m_data = nullptr;
}

В противном случае шаблон класса std :: vector будет вызывать конструктор копирования.

0 голосов
/ 20 февраля 2020

Добавьте noexcept декораторов к вашему конструктору перемещения и оператору назначения перемещения:

VecOfInt(VecOfInt&& other) noexcept : m_size(other.m_size) {
    std::cout << "move..." << std::endl;
    m_data = other.m_data;
    other.m_data = nullptr;
}

VecOfInt& operator=(VecOfInt&& other) noexcept {
    std::cout << "move assignment..." << std::endl;
    m_size = other.m_size;
    m_data = other.m_data;
    other.m_data = nullptr;
    return *this;
}

Некоторые функции (например, std::move_if_noexcept, используемые std::vector) будут определять скопировать ваш объект, если его операции перемещения не украшены noexcept., т.е. нет гарантии, что они не будут выбрасывать. Вот почему вы должны стремиться к тому, чтобы ваши операции перемещения (конструктор перемещения, оператор присваивания перемещения) не исключались. Это может значительно улучшить производительность вашей программы.

Согласно Скотту Мейеру в Effective Modern C ++:

std::vector использует этот «ход», если вы можете, но копируйте, если Вы должны "стратегию, и это не единственная функция в стандартной библиотеке, которая делает. Другие функции со строгим исключением безопасности в C ++ 98 (например, std::vector::reserve, std::deque::insert, et c) ведут себя так же. Все эти функции заменяют вызовы операций копирования в C ++ 98 вызовами операций перемещения в C ++ 11, только если известно, что операции перемещения не генерируют исключения. Но как функция может знать, что операция перемещения не вызовет исключения? Ответ очевиден: он проверяет, объявлена ​​ли операция noexcept.

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