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


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

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;
}

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

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

4 21

4 ответа:

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

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

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

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
};

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

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

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

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];
};

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

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

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

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

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

Чтобы лучше понять, когда move-semantic становится необходимым, рассмотрите возможность создания _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;
};