10

我最近和 Bjarne Stoustrup 一起参加了一场讲座,他在谈论 c++ 11 以及它为什么有意义。

他的一个新的令人敬畏的例子是移动构造函数的新闻'&&'符号。

然后我想回家并开始思考,“我什么时候需要这样的东西?”。

我的第一个例子是下面的代码:

class Number {
private:
    int value;
public:
    Number(const int value) : value(value){
        cout << "Build Constructor on " << value << endl;
    }
    Number(const Number& orig) : value(orig.value){
        cout << "Copy Constructor on " << value << endl;        
    }
    virtual ~Number(){}

    int toInt() const{
        return value;
    }


    friend const Number operator+(const Number& n0, const Number& n1); 
};

const Number operator+(const Number& n0, const Number& n1){
    return  Number(n0.value + n1.value);
}

int main(int argc, char** argv) {

    const Number n3 = (Number(2) + Number(1));
    cout << n3.toInt() << endl;
    return 0;
}

这段代码正是移动构造函数应该解决的问题。n3 变量由对“+”运算符返回的值的引用构成。

除了这是运行代码的输出:

Build Constructor on 1
Build Constructor on 2
Build Constructor on 3
3

RUN SUCCESSFUL 

输出显示的是复制构造函数永远不会被调用——这是在优化关闭的情况下。我很难扭曲代码的手臂以使其运行复制构造器。将结果包装在 std::pair 中就可以了,但这让我一直在思考。

运算符算术中移动构造函数的参数实际上是一个失败的参数吗?

为什么不调用我的复制构造函数以及为什么调用它:

using namespace std;

class Number {
private:
    int value;
public:
    Number(const int value) : value(value){
        cout << "Build Constructor on " << value << endl;
    }
    Number(const Number& orig) : value(orig.value){
        cout << "Copy Constructor on " << value << endl;        
    }
    virtual ~Number(){}

    int toInt() const{
        return value;
    }


    friend const std::pair<const Number, const Number> operator+(const Number& n0, const Number& n1); 
};

const std::pair<const Number, const Number> operator+(const Number& n0, const Number& n1){
    return  make_pair(Number(n0.value + n1.value), n0);
}

int main(int argc, char** argv) {

    const Number n3 = (Number(2) + Number(1)).first;
    cout << n3.toInt() << endl;
    return 0;
}

带输出:

Build Constructor on 1
Build Constructor on 2
Copy Constructor on 2
Build Constructor on 3
Copy Constructor on 3
Copy Constructor on 2
Copy Constructor on 3
Copy Constructor on 2
Copy Constructor on 3
3

RUN SUCCESSFUL 

我想知道逻辑是什么以及为什么配对运算符基本上会破坏性能?

更新:

我做了另一项修改,发现如果我替换make_pair为该对的实际模板化构造函数,pair<const Number, const Number>这会减少复制构造函数被触发的次数:

class Number {
private:
    int value;
public:
    Number(const int value) : value(value){
        cout << "Build Constructor on " << value << endl;
    }
    Number(const Number& orig) : value(orig.value){
        cout << "Copy Constructor on " << value << endl;        
    }
    virtual ~Number(){}

    int toInt() const{
        return value;
    }


    friend const std::pair<const Number, const Number> operator+(const Number& n0, const Number& n1); 
};



const std::pair<const Number, const Number> operator+(const Number& n0, const Number& n1){
    return  std::pair<const Number, const Number>(Number(n0.value + n1.value), n0);
}

int main(int argc, char** argv) {

    const Number n3 = (Number(2) + Number(1)).first;
    cout << n3.toInt() << endl;
    return 0;
}

输出 :

Build Constructor on 1
Build Constructor on 2
Build Constructor on 3
Copy Constructor on 3
Copy Constructor on 2
Copy Constructor on 3
3

RUN SUCCESSFUL

所以看起来使用make_pair是有害的吗?

4

2 回答 2

16

考虑这个简单的 C++ 代码:

class StringHolder
{
  std::string member;
public:

  StringHolder(const std::string &newMember) : member(newMember) {}
};

std::string value = "I am a string that will probably be heap-allocated.";
StringHolder hold(value);

执行第二行后,该字符串存在多少份?答案是两个:一个存储在 中value,一个存储在 中hold。那很好……有时。有时您想给某人一个字符串的副本,同时自己保留它。但有时你也不想这样做。例如:

StringHolder hold("I am a string that will probably be heap-allocated.");

这将创建一个std::string临时的,然后将其传递给StringHolder的构造函数。构造函数将复制构造它的成员。构造函数完成后,临时对象将被销毁。有一次,我们无缘无故地拥有了两份字符串。

拥有两个字符串副本是没有意义的。我们想要做的是std::string参数移动到 中,StringHolder这样字符串就只有一个副本。

这就是移动构造的用武之地。

Astd::string基本上只是一个包装指向已分配字符数组的指针,以及包含该数组长度(和容量,但现在不用管它)的大小。如果你有一个std::string,并且你想把它移到另一个,那么新字符串必须声明对该分配的字符数组的所有权,并且旧字符串必须放弃所有权。在 C++03 中,您可以通过以下操作来做到这一点swap

std::string oldStr = "I am a string that will probably be heap-allocated.";
std::string newStr;
std::swap(newStr, oldStr);

这会将 的内容移动oldStrnewStr没有任何内存分配的情况下。

C++11 的移动语法提供了两个没有的重要特性std::swap

首先,移动可以隐式发生(但只有在这样做是安全的时候)。swap如果要交换,必须显式调用;移动可以通过编写自然代码来实现。例如,取我们StringHolder之前的内容并进行一项更改:

class StringHolder
{
  std::string member;
public:

  StringHolder(std::string newMember) : member(std::move(newMember)) {}
};

StringHolder hold("I am a string that will probably be heap-allocated.");

这个字符串有多少个副本被创建?答案是……只有一个:临时建造。因为它是临时的,所以 C++11 足够聪明,知道它可以移动构造任何被它初始化的东西。所以它移动构造构造函数的值参数StringHolder(或者更可能完全省略构造)。这会将存储的内存从临时移动到newMember. 所以不会发生复制。

之后,我们在构造时显式调用移动构造函数member。这再次将分配的内存从 移动newMembermember

我们只分配一次字符串这可以大大节省性能。

现在,这与您自己类型的构造函数有什么关系?好吧,考虑一下这段代码:

class StringHolder
{
  std::string member;
public:

  StringHolder(std::string newMember) : member(std::move(newMember)) {}

  StringHolder(const StringHolder &old) : member(old.member) {}
  StringHolder(StringHolder &&old) : member(std::move(old.member)) {}
};

StringHolder oldHold = std::string("I am a string that will probably be heap-allocated.");
StringHolder newHold(oldHold);

这一次,我们现在有了一个带有复制和移动构造函数的类。我们得到多少个字符串副本?

二。当然是两个。我们有oldHoldand newHold,每个都有一个字符串的副本。

但是,如果我们这样做:

StringHolder oldHold = std::string("I am a string that will probably be heap-allocated.");
StringHolder newHold(std::move(oldHold));

然后再一次只有一个字符串的副本。

这就是为什么运动很重要。这就是它重要的原因:它减少了您可能需要放置的东西的副本数量。


为什么我的复制构造函数没有被调用

您的复制构造函数没有被调用,因为它被省略了。它正在做返回值优化。关闭优化不会有帮助,因为大多数编译器无论如何都会忽略。当省略是可能的时,没有理由不这样做。

对于函数返回值,在无法进行省略的情况下,移动很重要。

于 2012-05-20T11:36:32.323 回答
2

operator+如果你用这个重载你,它可能有助于理解移动语义的价值:

Number operator+(Number&& n0, Number&& n1){
  n0.value += n1.value;
  return std::move(n0);
}

这有两个重要的变化:

  • 它返回一个非常量值
  • 它接受右值参数并修改其中一个而不是创建一个新对象

这允许您的示例避免“构建构造函数”调用之一

Build Constructor on 1
Build Constructor on 2
Copy Constructor on 3
3

现在代码创建了两个新对象并复制了一个,而不是创建三个新对象,因此到目前为止并不是一个很大的优势。但是如果你添加一个可以用来代替副本的移动构造函数:

Number(Number&& orig) : value(orig.value){
  cout << "Move Constructor on " << value << endl;
}

Build Constructor on 1
Build Constructor on 2
Move Constructor on 3
3

如果该类在“Build”和“Copy”构造函数中分配内存,则您已将原始代码中的分配总数从三个减少到两个(假设移动构造函数不分配任何内容,但取得了内存的所有权由它移动的对象拥有。)

现在,如果您将计算更改为:

Number n3 = Number(2) + Number(1) + Number(0);

And compare your original code with the move-enabled version you should see the number of "allocations" reduced from five to three. The more temporaries that are involved the greater the benefit of modifying and moving from temporaries instead of creating new objects. The benefit is not only avoiding copies, but also avoiding creating new resources for new objects, by taking ownership of the resources from existing objects instead.

于 2012-05-20T23:13:17.347 回答