copy-/move-constructors的奇怪行为以及如何返回大对象?

Strange behavior of copy-/move-constructors & how to return large objects?

本文关键字:何返回 返回 对象 move-constructors copy-      更新时间:2023-10-16

最近,在缺席一段时间后,我再次尝试使用C++11。在阅读了互联网上的许多文章后,我现在完全不知道从工厂函数(基本上是从数据库中进行数据分析)返回大型对象的最有效方法是什么。

我已经成为unique_ptr的粉丝,但我在几篇文章中读到,由于有了新的move构造函数,现在完全可以通过值返回一个大向量,比如,并且由于这些新的语义,应该像复制一个指针一样快。

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

#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 fooValn";
    string str = "value return";
    C c(str);
    return c;
}
C& fooRef() {
    cout << "In fooRefn";
    string str = "reference return";
    C* pC = new C( str );
    return *pC;
}
C* fooPtr() {
    cout << "In fooPtrn";
    string str = "classical pointer return";
    C* pC = new C( str );
    return pC;
}
unique_ptr<C> fooUPtr() {
    cout << "In fooUPtrn";
    string str = "unique_ptr return";
    return unique_ptr<C>(new C(str));
}
shared_ptr<C> fooSPtr() {
    cout << "In fooSPtrn";
    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 constructedn";    
    C& cr = fooRef();        
    cout << "cr constructedn";        
    C* pC = fooPtr();        
    cout << "*pC constructedn";        
    unique_ptr<C> upC = fooUPtr();        
    cout << "*upC constructedn";        
    shared_ptr<C> spC = fooSPtr();        
    cout << "*spC constructedn";        
    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构造函数编译了它(我使用的是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构造函数不会改变语义,只会改变所涉及的构造函数数量。

因此我问:

  1. 发生了什么事?为什么value对象会"丢失"其字符串参数?在哪里
  2. 看起来价值回报有问题,而其他回报运作良好。那么,为什么现在人们建议我们按价值回归,"系统会照顾剩下的"呢
  3. 返回大型对象的好方法是什么
  4. 我是不是在一个我没有看到的地方犯了一个错误

谢谢!

看起来是这个clang bug:http://llvm.org/bugs/show_bug.cgi?id=12208其在简化的同时是与字符串串接相关的并且显然仍然不是固定的。

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

因为它会立即在我的系统上崩溃,而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的"阅读文档"的情况。我还没查。

这就是我编写fooVal:的方式

C fooVal() 
{    
    cout << "In fooValn";
    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";
};