Подтвердить что ты не робот

Перемещение конструкторов и статических массивов

Я изучал возможности Move Constructors на С++, и мне было интересно, какие способы использовать эту функцию в пример, такой как ниже. Рассмотрим этот код:

template<unsigned int N>
class Foo {
public:
    Foo() {
        for (int i = 0; i < N; ++i) _nums[i] = 0;
    }

    Foo(const Foo<N>& other) {
        for (int i = 0; i < N; ++i) _nums[i] = other._nums[i];
    }

    Foo(Foo<N>&& other) {
        // ??? How can we take advantage of move constructors here?
    }

    // ... other methods and members

    virtual ~Foo() { /* no action required */ }

private:
    int _nums[N];
};

Foo<5> bar() {
    Foo<5> result;
    // Do stuff with 'result'
    return result;
}

int main() {
    Foo<5> foo(bar());
    // ...
    return 0;
}

В этом выше примере, если мы трассируем программу (с MSVС++ 2011), мы видим, что при построении foo вызывается Foo<N>::Foo(Foo<N>&&), что является желаемым поведением. Однако, если бы у нас не было Foo<N>::Foo(Foo<N>&&), вместо него было бы вызвано Foo<N>::Foo(const Foo<N>&), что сделало бы операцию избыточной копии.

Мой вопрос, как отмечено в коде, с этим конкретным примером, который использует статически распределенный простой массив, есть ли способ использовать конструктор перемещения, чтобы избежать этой избыточной копии?

4b9b3361

Ответ 1

Во-первых, есть общий совет, в котором говорится, что вы не должны писать какой-либо конструктор копирования/перемещения, оператор присваивания или деструктор вообще, если вы можете ему помочь, и, скорее, составить свой класс высококачественных компонентов, которые, в свою очередь, предоставляйте их, позволяя функциям, созданным по умолчанию, делать правильную вещь. (Обратное следствие состоит в том, что если вам нужно написать какой-либо из них, вам, вероятно, придется написать все из них.)

Итак, вопрос сводится к тому, "какой компонент компонента одной ответственности может использовать семантику перемещения?" Общий ответ: все, что управляет ресурсом. Дело в том, что конструктор/получатель перемещения просто переустанавливает ресурс на новый объект и делает недействительным старый, что позволяет избежать (предполагаемого дорогого или невозможного) нового распределения и глубокого копирования ресурса.

Главный пример - это все, что управляет динамической памятью, где операция перемещения просто копирует указатель и устанавливает старый указатель объекта на ноль (так что деструктор старого объекта ничего не делает). Здесь наивный пример:

class MySpace
{
  void * addr;
  std::size_t len;

public:
  explicit MySpace(std::size_t n) : addr(::operator new(n)), len(n) { }

  ~MySpace() { ::operator delete(addr); }

  MySpace(const MySpace & rhs) : addr(::operator new(rhs.len)), len(rhs.len)
  { /* copy memory */ }

  MySpace(MySpace && rhs) : addr(rhs.addr), len(rhs.len)
  { rhs.len = 0; rhs.addr = 0; }

  // ditto for assignment
};

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

Ответ 2

В этом случае это не полезно, потому что int не имеет конструкторов move.

Однако было бы полезно, если бы это были строки, например:

template<unsigned int N>
class Foo {
public:
    // [snip]

    Foo(Foo<N>&& other) {
        // move each element from other._nums to _nums
        std::move(std::begin(other._nums), std::end(other._nums), &_nums[0]);
    }

    // [snip]

private:
    std::string _nums[N];
};

Теперь вы избегаете копирования строк, где будет выполняться перемещение. Я не уверен, что соответствующий компилятор С++ 11 будет генерировать эквивалентный код, если вы полностью опустите все конструкторы copy/move, извините.

(Другими словами, я не уверен, что std::move специально определен для элементарного перемещения для массивов.)

Ответ 3

Для шаблона класса, который вы написали, нет никакого преимущества в создании конструктора перемещения.

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

Ответ 4

Обычно move-semantic реализуется, когда ваш класс управляет ресурсом. Поскольку в вашем случае класс не управляет ресурсом, семантика move будет больше похожа на семантику семантики, так как ничего не нужно перемещать.

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

template<unsigned int N>
class Foo {
public:
    Foo() 
    {
        _nums = new int[N](); //allocate and zeo-initialized
    }
    Foo(const Foo<N>& other) 
    {
        _nums = new int[N];
        for (int i = 0; i < N; ++i) _nums[i] = other._nums[i];
    }

    Foo(Foo<N>&& other) 
    {
         _nums = other._nums; //move the resource
         other._nums=0; //make it null
    }

    Foo<N> operator=(const Foo<N> & other); //implement it!

    virtual ~Foo() { delete [] _nums; }

private:
    int *_nums;
};