28

我一直在探索 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>&)则会被调用,这将执行冗余复制操作。

我的问题是,如代码中所述,对于这个使用静态分配的简单数组的特定示例,有没有办法利用移动构造函数来避免这种冗余副本?

4

4 回答 4

26

首先,有一种通用的建议说,如果你能提供帮助,你根本不应该编写任何复制/移动构造函数、赋值运算符或析构函数,而是编写的高质量组件类,这些组件反过来提供这些,允许默认生成的函数做正确的事情。(相反的含义是,如果您确实必须编写其中任何一个,则可能必须编写所有这些。)

所以问题归结为“哪个单一职责组件类可以利用移动语义?” 一般的答案是:任何管理资源的东西。关键是移动构造函数/分配器只会将资源重新分配给新对象并使旧对象无效,从而避免(假定昂贵或不可能的)新分配和资源的深度复制。

最好的例子是管理动态内存的任何东西,其中移动操作只是复制指针并将旧对象的指针设置为零(因此旧对象的析构函数什么都不做)。这是一个天真的例子:

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

关键是任何复制/移动构造函数都会对成员变量进行完整复制;只有当这些变量本身是句柄或指向资源的指针时,您才能避免复制资源,因为协议认为移动的对象不再被认为是有效的,并且您可以自由地从中窃取。如果没有什么可偷的,那么搬家就没有好处了。

于 2011-10-25T06:42:36.330 回答
9

在这种情况下,它没有用,因为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专门定义为对数组进行元素移动。)

于 2011-10-25T06:54:39.230 回答
8

对于您编写的类模板,采用移动构造函数没有任何好处。

如果成员数组是动态分配的,那将是一个优势。但是使用普通数组作为成员,没有什么可优化的,您只能复制值。没有办法移动它们。

于 2011-10-25T06:42:14.757 回答
2

通常,移动语义是在您的类管理 资源时实现的。由于在您的情况下,该类不管理资源,因此 move-semantic 将更像是 copy-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;
};
于 2011-10-25T06:47:46.087 回答