移动语义:如何最好地 understand/use 它们

move semantics: how best to understand/use them

我在 C++11 的移动语义方面遇到问题。我正在使用 gcc 4.9.2 20150304(预发布)-std=c++11 开关,但我在移动构造函数中遇到问题未被调用。

我有以下源文件:

#ifndef DENSEMATRIX_H_
#define DENSEMATRIX_H_

#include <cstddef>
#include <iostream>
#include <utility>

class DenseMatrix {
    private:
        size_t m_ = 0, n_ = 0;
        double *values = nullptr;
    public:
        /* ctor */
        DenseMatrix( size_t, size_t );
        /* copy ctor */
        DenseMatrix( const DenseMatrix& rhs );
        /* move ctor */
        DenseMatrix( DenseMatrix&& rhs ) noexcept;
        /* copy assignment */
        const DenseMatrix& operator=( const DenseMatrix& rhs );
        /* move assignment */
        const DenseMatrix& operator=( DenseMatrix&& rhs ) noexcept;
        /* matrix multiplication */
        DenseMatrix operator*( const DenseMatrix& rhs ) const;
        /* dtor */
        ~DenseMatrix();
};

#endif
#include "densematrix.h"

/* ctor */
DenseMatrix::DenseMatrix( size_t m, size_t n ) :
    m_( m ), n_( n ) {
    std::cout << "ctor with two arguments called." << std::endl;
    if ( m_*n_ > 0 )
        values = new double[ m_*n_ ];
}

/* copy ctor */
DenseMatrix::DenseMatrix( const DenseMatrix& rhs ) :
    m_( rhs.m_ ), n_( rhs.n_ ) {
    std::cout << "copy ctor called." << std::endl;
    if ( m_*n_ > 0 ) {
        values = new double[ m_*n_ ];

        std::copy( rhs.values, rhs.values + m_*n_, values);
    }
}

/* move ctor */
DenseMatrix::DenseMatrix( DenseMatrix&& rhs ) noexcept :
    m_( rhs.m_ ), n_( rhs.n_ ), values( rhs.values ) {
    std::cout << "move ctor called." << std::endl;
    rhs.values = nullptr;
}

/* copy assignment */
const DenseMatrix& DenseMatrix::operator=( const DenseMatrix& rhs ) {
    std::cout << "copy assignment called." << std::endl;

    if ( this != &rhs ) {
        if ( m_*n_ != rhs.m_*rhs.n_ ) {
            delete[] values;
            values = new double[ rhs.m_*rhs.n_ ];
        }

        m_ = rhs.m_;
        n_ = rhs.n_;
        std::copy( rhs.values, rhs.values + m_*n_, values);
    }

    return *this;
}

/* move assignment */
const DenseMatrix& DenseMatrix::operator=( DenseMatrix&& rhs ) noexcept {
    std::cout << "move assignment called." << std::endl;

    m_ = rhs.m_;
    n_ = rhs.n_;
    delete[] values;
    values = rhs.values;
    rhs.values = nullptr;

    return *this;
}

/* matrix multiplication */
DenseMatrix DenseMatrix::operator*( const DenseMatrix& rhs ) const {
    return DenseMatrix( this->m_, rhs.n_ );
}

/* dtor */
DenseMatrix::~DenseMatrix() {
    std::cout << "dtor called." << std::endl;
    delete[] values;
}
#include <iostream>
#include <utility>
#include "densematrix.h"

int main( int argc, char* argv[] ) {
    /* ctor */
    DenseMatrix A( 5, 10 );
    /* ctor */
    DenseMatrix B( 10, argc );
    /* copy ctor */
    DenseMatrix C = A;
    /* copy assignment */
    C = B;
    /* move ctor */
    DenseMatrix D( A*B );
    DenseMatrix E = DenseMatrix( 100, 200 );
    /* move assignment */
    D = C*D;

    return 0;
}

如果我在没有 -fno-elide-constructors 开关的情况下编译我的程序,我将获得以下输出:

ctor with two arguments called.
ctor with two arguments called.
copy ctor called.
copy assignment called.
ctor with two arguments called.
ctor with two arguments called.
ctor with two arguments called.
move assignment called.
dtor called.
dtor called.
dtor called.
dtor called.
dtor called.
dtor called.

另一方面,如果我使用 -fno-elide-constructors 开关进行编译,我会得到以下输出:

ctor with two arguments called.
ctor with two arguments called.
copy ctor called.
copy assignment called.
ctor with two arguments called.
move ctor called.
dtor called.
move ctor called.
dtor called.
ctor with two arguments called.
move ctor called.
dtor called.
ctor with two arguments called.
move ctor called.
dtor called.
move assignment called.
dtor called.
dtor called.
dtor called.
dtor called.
dtor called.
dtor called.

在第二个输出中,我对 move ctor 感到困惑。首先,在创建 D 时调用了两个 move ctor,而在创建 E 时只调用了一个 move ctor。其次,在将乘法结果赋给 D 时,在 move 赋值运算符之前调用了另一个 move ctor。

如果我的 class 设计正确,有人可以解释一下发生了什么吗 and/or?遇到这种情况怎么办?我应该以正常方式编译程序(没有开关)并在我想确保移动语义或什么时使用 std::move() 吗?

谢谢!

根据@Praetorian 的评论进行编辑:
我的 densematrix.h 实现的最终版本如下:

#ifndef DENSEMATRIX_H_
#define DENSEMATRIX_H_

#include <cstddef>
#include <stdexcept>
#include <utility>
#include <vector>

class DenseMatrix {
    private:
        size_t m_ = 0,  /* number of rows */
               n_ = 0;  /* number of columns */
        /* values of the matrix in column major order */
        std::vector< double > values_;
    public:
        /* ctor */
        DenseMatrix( size_t m, size_t n,
            std::vector< double > values = std::vector< double >() ) :
            m_( m ),
            n_( n ),
            values_( values )
        {
            if ( m_*n_ == 0 )
                throw std::domain_error( "One of the matrix dimensions is zero!" );
            else if ( m_*n_ != values.size() && values_.size() != 0 )
                throw std::domain_error( "Matrix dimensions do not match with the number of elements" );
        }
        /* copy ctor */
        DenseMatrix( const DenseMatrix& rhs ) :
            m_( rhs.m_ ),
            n_( rhs.n_ ),
            values_( rhs.values_ ) { }
        /* move ctor */
        DenseMatrix( DenseMatrix&& rhs ) noexcept :
            m_( std::move( rhs.m_ ) ),
            n_( std::move( rhs.n_ ) ),
            values_( std::move( rhs.values_ ) ) { }
        /* copy assignment */
        const DenseMatrix& operator=( const DenseMatrix& rhs ) {
            if ( this != &rhs ) {
                m_ = rhs.m_;
                n_ = rhs.n_;
                /* trust std::vector<>'s optimized implementation, i.e.,
                 * no need to check the vectors' sizes to decrease the
                 * heap access */
                values_ = rhs.values_;
            }

            return *this;
        }
        /* move assignment */
        const DenseMatrix& operator=( DenseMatrix&& rhs ) noexcept {
            m_ = std::move( rhs.m_ );
            n_ = std::move( rhs.n_ );
            values_ = std::move( rhs.values_ );

            return *this;
        }
        /* matrix multiplication */
        DenseMatrix operator*( const DenseMatrix& rhs ) const {
            /* do dimension checking */
            DenseMatrix temp( this->m_, rhs.n_ );

            /* do the necessary calculations */

            return temp;
        }
        /* dtor not needed in this case */
};

#endif

现在,希望我已经正确地实现了语义。你怎么看?现在,在复制 and/or 不同大小的移动向量时,我依赖于我正在使用的容器 class。

再次感谢您的帮助和意见!

每次使用 operator* 时的两个移动操作是因为您要求编译器不要执行 copy/move 省略。这迫使它在 operator* 内部构造一个临时值(2 个参数构造函数调用),然后将这个临时值移动到 return 值(移动构造函数调用),最后将 return 值移动到目标对象(在您的示例中移动 constructor/move 赋值调用)。

我还打印了打印语句中涉及的对象的地址,从而使您的示例变得更加嘈杂。例如

std::cout << "move ctor called. " << this << std::endl;

Live demo

让我们看看这里发生了什么:

/* move ctor */
DenseMatrix D( A*B );
std::cout << "&D " << &D << std::endl;

相关输出语句为

ctor with two arguments called. 0x7fff7c2cb1d0  <-- temporary created in the 
                                                    return statement of operator*
move ctor called. 0x7fff7c2cb2b0                <-- temporary moved into the return value
dtor called. 0x7fff7c2cb1d0                     <-- temporary from step 1 destroyed
move ctor called. 0x7fff7c2cb230                <-- return value moved into D
dtor called. 0x7fff7c2cb2b0                     <-- return value destroyed
&D 0x7fff7c2cb230                               <-- address of D

D = C*D; 的输出是相同的,除了第二个移动构造被移动赋值代替。

您在 class 实现中没有做错任何事情,只是没有使用 -fno-elide-constructors 来编译代码(为什么会这样?)。

这对您的情况没有影响,但通常在移动构造函数中,源对象的数据成员在 mem-initializerstd::moved 中

DenseMatrix::DenseMatrix( DenseMatrix&& rhs ) noexcept :
    m_( std::move(rhs.m_) ), n_( std::move(rhs.n_) ), values( std::move(rhs.values) ) {
//..
}

最后,您可能希望将 valuesdouble* 更改为 std::vector<double> 并避免所有 newdelete 调用。这样做的唯一注意事项是 vector 将值初始化它在您 vector::resize 时添加的新元素,但 there are workarounds 也会这样做。


更新以解决您在上次编辑中添加的代码。

不仅不再需要析构函数定义,而且您也不需要任何 copy/move constructors/assignment 运算符。编译器会隐式地为您声明这些,它们的作用与您的手写版本完全相同。因此,您 class 中需要的只是构造函数定义和 operator* 的构造函数定义。这就是不手动管理内存的真正美妙之处。

我会对构造函数定义做一些更改

    DenseMatrix( size_t m, size_t n,
        std::vector< double > values = {} ) : // <-- use list initialization, 
                                              //     no need to repeat type name
        m_( m ),
        n_( n ),
        values_( std::move(values) )          // <-- move the vector instead of copying
    {
        // ...
    }