移动语义:如何最好地理解/使用它们

时间:2015-04-23 13:11:04

标签: c++ c++11 move-semantics

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

我有以下源文件:

  • densematrix.h
#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
  • densematrix.cpp
#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;
}
  • matrix_trial.cpp
#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.

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

有人可以解释发生了什么和/或我是否正确设计了我的课程?在这种情况下我该怎么办?我应该以正常的方式编译程序(没有开关)并且每当我想确保移动语义或什么时使用 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

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

再次感谢您的帮助和评论!

1 个答案:

答案 0 :(得分:2)

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

通过打印print语句中涉及的对象的地址,我的例子有点吵闹。例如

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;的输出是相同的,除了第二个移动构造被移动赋值替换。

你的类实现没有做错,只是不要使用-fno-elide-constructors来编译代码(为什么会这样?)。

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

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也是如此。

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

不仅不再需要析构函数定义,而且您不需要任何复制/移动构造函数/赋值运算符。编译器将隐式为您声明这些,并且它们完全与您的手写版本相同。所以你所需要的只是构造函数定义和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
    {
        // ...
    }