复制/移动构造函数的奇怪行为&如何返回大型物体?

时间:2013-08-19 16:49:08

标签: c++ c++11 constructor copy-constructor move-constructor

我最近再次尝试使用C ++ 11,经过一段时间的缺席,在网上阅读了很多文章后,我现在对从工厂函数返回大型对象的最有效方法感到困惑(基本上,数据)从数据库分析)。

我已成为unique_ptr的粉丝,但我在几篇文章中读到,由于新的移动构造函数,现在完全有可能返回一个大的向量说按值并因为这些新的语义它应该和复制一个指针一样快。

为了尝试这一点,我在各种构造函数中编写了一个带有输出的小测试程序:

#include <iostream>
#include <memory>

using namespace std;


class C {
public:
    C( string n ) : _name{n} { cout << "Constructing a C named '" << _name << "'\n"; };

    C() : _name( "EMPTY" ) { cout << "Default-constructing a C named '" << _name << "'\n"; } ;     // default-ctor

    C( const C& c ) : _name{c._name} {
        _name += " [copied]";
        cout << "Copy-constructing a C named '" << _name << "'\n";
    };

    C( C&& c )
        : _name{c._name} {
        _name += " [moved]";
        cout << "Move-constructing a C named '" << _name << "'\n";
    };

    ~C() { cout << "Destructing a C named '" << _name << "'\n"; };

    string getName() { return _name; };

private:
    string _name;
};

并使用

进行测试
C fooVal() {    
    cout << "In fooVal\n";
    string str = "value return";
    C c(str);
    return c;
}

C& fooRef() {
    cout << "In fooRef\n";
    string str = "reference return";
    C* pC = new C( str );
    return *pC;
}

C* fooPtr() {
    cout << "In fooPtr\n";
    string str = "classical pointer return";
    C* pC = new C( str );
    return pC;
}

unique_ptr<C> fooUPtr() {
    cout << "In fooUPtr\n";
    string str = "unique_ptr return";
    return unique_ptr<C>(new C(str));
}

shared_ptr<C> fooSPtr() {
    cout << "In fooSPtr\n";
    string str = "shared_ptr return";
    return shared_ptr<C>(new C(str));
}

// IMPORTANT: THIS NEEDS TO BE COMPILED WITH FLAG -fno-elide-constructors
int main(int argc, const char * argv[])
{
    C cv(fooVal());        
    cout << "cv constructed\n";    
    C& cr = fooRef();        
    cout << "cr constructed\n";        
    C* pC = fooPtr();        
    cout << "*pC constructed\n";        
    unique_ptr<C> upC = fooUPtr();        
    cout << "*upC constructed\n";        
    shared_ptr<C> spC = fooSPtr();        
    cout << "*spC constructed\n";        
    cout << "Alive: " << cv.getName() << ", " << cr.getName() << ", " << pC->getName() << ", " << upC->getName() << ".\n";
}

现在,如果我按原样编译它,编译器会优化(“elides”)各种构造函数调用并得到输出:

In fooVal
Constructing a C named 'value return'
cv constructed
In fooRef
Constructing a C named 'reference return'
cr constructed
In fooPtr
Constructing a C named 'classical pointer return'
*pC constructed
In fooUPtr
Constructing a C named 'unique_ptr return'
*upC constructed
In fooSPtr
Constructing a C named 'shared_ptr return'
*spC constructed
Alive: value return, reference return, classical pointer return, unique_ptr return.
Destructing a C named 'shared_ptr return'
Destructing a C named 'unique_ptr return'
Destructing a C named 'value return'

好的,但你可以看到已经优化了多少复制。要查看“指定”行为是什么,我使用标志-fno-elide-constructors(我正在使用Apple LLVM版本4.2(clang-425.0.28))编译它。但后来我得到以下输出:

In fooVal
Constructing a C named 'value return'
Destructing a C named 'value return'
Move-constructing a C named ' [moved]'
Destructing a C named ''
cv constructed
In fooRef
Constructing a C named 'reference return'
cr constructed
In fooPtr
Constructing a C named 'classical pointer return'
*pC constructed
In fooUPtr
Constructing a C named 'unique_ptr return'
*upC constructed
In fooSPtr
Constructing a C named 'shared_ptr return'
*spC constructed
Alive:  [moved], reference return, classical pointer return, unique_ptr return.
Destructing a C named 'shared_ptr return'
Destructing a C named 'unique_ptr return'
Destructing a C named ' [moved]'

所以,很明显,返回值的对象正在发生一些可疑的事情。显然,这不仅仅是一个小问题,因为我希望-fno-elide-constructors不要改变语义,只改变所涉及的构造函数。

因此我问:

  1. 发生了什么事?为什么值对象“丢失”其字符串参数?在哪里?
  2. 看起来价值回报有问题,而其他的回报则很好。那么,为什么人们现在推荐我们按价值回归并且“系统会照顾其余部分”?
  3. 什么是返回大型物体的好方法?
  4. 我在某个地方犯了一个我没有看到的错误吗?
  5. 谢谢!

3 个答案:

答案 0 :(得分:3)

看起来就是这个clang bug:http://llvm.org/bugs/show_bug.cgi?id=12208虽然简化了字符串连接,但显然仍然没有修复。

答案 1 :(得分:1)

老实说,我认为-fno-elide-constructors不会产生有效的程序。

因为它会立即在我的系统上崩溃,并且valgrind很快指出了重大错误:

==6098== Memcheck, a memory error detector
==6098== Copyright (C) 2002-2011, and GNU GPL'd, by Julian Seward et al.
==6098== Using Valgrind-3.7.0 and LibVEX; rerun with -h for copyright info
==6098== Command: ./test
==6098== 
In fooVal
Constructing a C named 'value return'
Destructing a C named 'value return'
==6098== Use of uninitialised value of size 8
==6098==    at 0x4EEF83B: std::basic_string<char, std::char_traits<char>, std::allocator<char> >::basic_string(std::string const&) (in /usr/lib/x86_64-linux-gnu/libstdc++.so.6.0.17)
==6098==    by 0x40297C: C::C(C&&) (test.cpp:19)
==6098==    by 0x401C0C: C::C(C&&) (test.cpp:22)
==6098==    by 0x40165D: main (test.cpp:69)
==6098== 
==6098== Conditional jump or move depends on uninitialised value(s)
==6098==    at 0x4EEF84D: std::basic_string<char, std::char_traits<char>, std::allocator<char> >::basic_string(std::string const&) (in /usr/lib/x86_64-linux-gnu/libstdc++.so.6.0.17)
==6098==    by 0x40297C: C::C(C&&) (test.cpp:19)
==6098==    by 0x401C0C: C::C(C&&) (test.cpp:22)
==6098==    by 0x40165D: main (test.cpp:69)

这是使用

Ubuntu clang version 3.2-9 (tags/RELEASE_32/final) (based on LLVM 3.2)
Target: x86_64-pc-linux-gnu
Thread model: posix

这可能是编译器错误,或者可能是关于-fno-elide-constructors的“阅读文档”的情况。我没有检查过。

答案 2 :(得分:-1)

这就是我写fooVal的方式:

C fooVal() 
{    
    cout << "In fooVal\n";
    return C("value return");
}

..这就是我写C::C(C&&)的方式 - 虽然它应该在这种情况下自动生成...

C( C&& c ) : _name{std::move(c._name)} //  note the "std::move"
{
    _name += " [moved]";
    cout << "Move-constructing a C named '" << _name << "'\n";
};