C++11 右值引用与g++ fno-elide-constructors编译选项

本文深入探讨了C++11引入的右值引用概念及其应用场景。解释了左值、右值的区别,并通过实例展示了如何利用右值引用来提高代码效率。

右值和将亡值        

C++11增加了一种新类型,称为右值引用(R-value-reference),标记为T &&。那么怎么区分左右值?左值指的是表达式结束后依然存在的持久对象,右值指的是表达式结束时就不再存在的临时对象。一个区分左值与右值的便捷方法是:看能不能对表达式取地址,如果能,则为左值,否则为右值。所有的具名变量或者对象都是左值,而右值不具名。

       在C++11中,右值由两个概念构成,一个是将亡值(xvalue, expiring value),另一个是纯右值(prvalue, PureRvalue),比如,非引用返回的临时对象、运算变动时产生的临时变量、原始字面量和lambda表达式等都是纯右值。

       C++11中所有值必属于左值、将亡值、纯右值三者之一。

右值引用

        在C++中,如果去引用一个返回临时对象,那么结果是不可预知的,因为随着函数的退出,所引用的临时对象也会被析构,这个时候引用的其实是一片垃圾空间,如下代码。

Object CreateObject()
{
	return Object();
}
Object &obj = CreateObject();

       在学校考试或者面试的时候会经常问这段代码有没有问题,那问题就是引用了一个临时变量。那么通常的做法是不引用,直接拷贝就可以避免上述问题。但新问题是这样会调用一次拷贝构造函数,这带来了不必要的开销,所以很多时候我们会传一个引用参数,函数类型类似于:

void CreateObject(MyObject &object)

      这样写的弊端在于增加了函数参数个数。研究发现,第一段代码返回了一个临时对象,这完全符合C++11中右值的定义,所以我们可以用右值引用来解决上面问题,代码如下:

Object CreateObject()
{
	return Object();
}
Object &&obj = CreateObject();

&表示左值引用,而&&则表示右值引用。通过右值引用的声明,该右值又“重获新生”,其生命周期与右值引用类型变量的生命周期一样。下面通过实际例子来验证右值引用。

class Student
{
    public:
        Student()
        {
            construct_count += 1;
            std::cout << "call construct" << std::endl;
        }
        ~Student()
        {
            destruct_count += 1;
            std::cout << "call destruct" << std::endl;
        }
        Student(const Student &other)
        {
            std::cout << "call copy construct" << std::endl;
            construct_count += 1;
        }
        Student &operator=(const Student &other)
        {
            if (this != &other)
            {
            }
            std::cout << "call = construct" << std::endl;

            construct_count += 1;
            return *this;
        }

        static int construct_count;
        static int destruct_count;
};

int Student::construct_count = 0;
int Student::destruct_count = 0;

Student CreateStudent()
{
    return Student();
}

int  main()
{
    do
    {
        Student student = CreateStudent();
    } while(0);

    std::cout << "construct=" << Student::construct_count << std::endl;
    std::cout << "destruct=" << Student::destruct_count << std::endl;

    return 0;
}

        按一般分析,这段代码应该调了3此构造函数,3次析构函数。第一次是return的时候创建对象,第二次是return的时候将创建的临时对象拷贝到函数调用栈空间,第三次是main函数空函数栈空间拷贝对象。用c++11执行编译并运行,g++ right_reference.cpp --std=c++11  -o test

      结果居然是1!这严重不符合我们的预期,在怎么算也不可能是1,我们试一下用右值引用,只需把main函数中获取函数值改成&&方式。

 Student &&student = CreateStudent();

然而并没有什么用,左右值引用根本没区别,难道是我们错了吗?仔细读书发现书上有一句是“关闭fno-elide-constructors”,fno-elide-constructors大概意思就是关闭编译器优化,不同的编译器优化策略并不一样,关闭选项再试看。

       在没有使用右值引用的时候这段代码调用了3此拷贝函数,而在右值引用的时候调用了2次,由此可见在编译器不做优化的情况下右值引用比普通调用更加高效。

#define _CRT_SECURE_NO_WARNINGS 1 #include<iostream> #include<assert.h> #include<string.h> #include<algorithm> using namespace std; namespace wusaqi { class string { public: typedef char* iterator; typedef const char* const_iterator; iterator begin() { return _str; } iterator end() { return _str + _size; } const_iterator begin() const { return _str; } const_iterator end() const { return _str + _size; } string(const char* str = "") :_size(strlen(str)) , _capacity(_size) { cout << "string(char* str)-构造" << endl; _str = new char[_capacity + 1]; strcpy(_str, str); } void swap(string& s) { ::swap(_str, s._str); ::swap(_size, s._size); ::swap(_capacity, s._capacity); } string(const string& s) :_str(nullptr) { cout << "string(const string& s) -- 拷⻉构造" << endl; reserve(s._capacity); for (auto ch : s) { push_back(ch); } } // 移动构造 string(string&& s) { cout << "string(string&& s) -- 移动构造" << endl; swap(s); } string& operator=(const string& s) { cout << "string& operator=(const string& s) -- 拷贝赋" << endl; if (this != &s) { _str[0] = '\0'; _size = 0; reserve(s._capacity); for (auto ch : s) { push_back(ch); } } return *this; } // 移动赋 string& operator=(string&& s) { cout << "string& operator=(string&& s) -- 移动赋" << endl; swap(s); return *this; } ~string() { cout << "~string() -- 析构" << endl; delete[] _str; _str = nullptr; } char& operator[](size_t pos) { assert(pos < _size); return _str[pos]; } void reserve(size_t n) { if (n > _capacity) { char* tmp = new char[n + 1]; if (_str) { strcpy(tmp, _str); delete[] _str; } _str = tmp; _capacity = n; } } void push_back(char ch) { if (_size >= _capacity) { size_t newcapacity = _capacity == 0 ? 4 : _capacity * 2; reserve(newcapacity); } _str[_size] = ch; ++_size; _str[_size] = '\0'; } string& operator+=(char ch) { push_back(ch); return *this; } const char* c_str() const { return _str; } size_t size() const { return _size; } private: char* _str = nullptr; size_t _size = 0; size_t _capacity = 0; }; } int main() { wusaqi::string s1("xxxxx"); // 拷贝构造 wusaqi::string s2 = s1; // 构造+移动构造,优化后直接构造 wusaqi::string s3 = wusaqi::string("yyyyy"); // 移动构造 wusaqi::string s4 = move(s1); return 0; } 为什么 wusaqi::string s3 = wusaqi::string(“yyyyy”);没有体现引用延长生命周期, 编译时加了-fno-elide-constructors 禁用复制省略
最新发布
08-30
<think> 首先,用户的问题是:为什么在代码中 `wusaqi::string s3 = wusaqi::string("yyyyy");` 没有体现引用延长生命周期?并且用户提到编译时加了 `-fno-elide-constructors` 禁用了复制省略。 关键点是“没有体现引用延长生命周期”。在C++中,引用和移动语义用于高效地处理临时对象。这里的代码实现了一个自定义的 `string` 类,包括移动构造函数和移动赋运算符。 回顾代码: - `wusaqi::string s3 = wusaqi::string("yyyyy");` 这是一个声明和初始化,使用了临时对象 `wusaqi::string("yyyyy")`。在C++11及以后,由于移动语义的支持,编译器通常可以优化这个初始化,避免不必要的拷贝。 用户指定加了 `-fno-elide-constructors` 选项,这禁用了编译器的复制省略(copy elision)。复制省略是C++17标准中的强制特性,但在更早的标准中是可选的。使用 `-fno-elide-constructors` 会阻止编译器优化掉临时对象的拷贝。 在代码中,`wusaqi::string` 类有移动构造函数: ```cpp string(string&& s) { cout << "string(string&& s) -- 移动构造" << endl; swap(s); } ``` 如果复制省略被禁用,那么 `s3` 的初始化应该首先构造一个临时对象(调用普通的构造函数),然后移动构造 `s3` 从这个临时对象。也就是说,输出应该显示: 1. 构造临时对象:输出 "string(char* str)-构造"(因为 `wusaqi::string("yyyyy")` 调用这个构造函数) 2. 然后调用移动构造函数来初始化 `s3`:输出 "string(string&& s) -- 移动构造" 用户说“没有体现引用延长生命周期”。这里的“延长生命周期”可能指的是引用如何允许临时对象的资源被移动而不是拷贝,从而避免额外开销。 但在代码中,`s3` 的初始化应该触发移动构造函数是因为临时对象是一个。 用户提到“没有体现”,意思是输出中没有显示移动构造?用户提供了代码,但在 `main` 函数中,有输出语句: 在构造函数中有 `cout` 输出。例如: - `string(const char* str)` 输出 "string(char* str)-构造" - 拷贝构造输出 "拷贝构造" - 移动构造输出 "移动构造" 在 `main` 函数中: ```cpp wusaqi::string s3 = wusaqi::string("yyyyy"); ``` 如果复制省略被禁用,输出应该包括构造和移动构造。 用户说“编译时加了 `-fno-elide-constructors` 禁用复制省略”,所以理论上,应该有临时对象的构造和移动构造。 但在代码中,`s3` 的初始化是直接初始化,临时对象是,所以应该调用移动构造函数。 回顾代码:在 `string` 类的移动构造函数中,有输出 "string(string&& s) -- 移动构造",所以如果调用了,应该输出这个消息。 用户的问题可能是:为什么在输出中没有看到移动构造的调用?或者为什么生命周期没有被延长? 在移动构造函数中,它调用 `swap(s)`,这交换了内部指针,有效地移动了资源。临时对象的析构函数会清理交换后的资源。 在 `string` 类的析构函数中也输出 "~string() -- 析构"。 所以,在 `s3` 初始化后,临时对象会被销毁,调用析构函数。 但用户说“没有体现引用延长生命周期”,可能误解了术语。“延长生命周期”通常不是引用的直接特性;相反,移动语义允许我们将临时对象的资源“窃取”过来,避免拷贝。 在C++中,引用可以绑定到临时对象,但不会延长其生命周期;生命周期仍然在完整表达式结束时结束。移动构造函数允许我们在临时对象销毁前移动其资源。 在 `s3 = wusaqi::string("yyyyy")` 中,临时对象在表达式结束时被销毁,但由于移动构造,资源被移动到 `s3`,所以没有深拷贝。 用户指定了 `-fno-elide-constructors`,所以在这种设置下,应该有两个步骤:构造临时对象,然后移动构造 `s3`。 查看用户提供的代码,在 `main` 中的注释说:"// 构造+移动构造,优化后直接构造",意思是如果优化启用,会直接构造 `s3`,但禁用优化后,应该有构造和移动构造。 用户的问题:为什么没有体现引用延长生命周期?可能用户期望移动构造被调用,但可能在实际运行中,由于某种原因没有被调用,或者用户看到了输出但没有移动构造的消息。 但在代码中,移动构造函数有输出,所以如果调用,应该可见。 或许在用户的编译环境中,即使禁用了复制省略,移动构造也没有被触发?但那不应该,因为临时对象是。 检查代码:在 `string` 类的构造函数中,`string(const char* str)` 是普通构造函数。 然后 `s3 = wusaqi::string("yyyyy")` 是一个声明和初始化,等同于 `wusaqi::string s3(wusaqi::string("yyyyy"))`,所以它应该调用移动构造函数,因为实参是临时对象。 在C++中,`T a = T(args);` 的语义是:如果复制省略被禁用,它会构造临时对象,然后拷贝或移动构造 `a`。由于 `string` 有移动构造函数,它应该优先选择移动而不是拷贝,因为临时对象是。 所以,在输出中,应该看到: - "string(char* str)-构造" for the temporary - "string(string&& s) -- 移动构造" for moving to s3 然后临时对象被销毁,输出 "~string() -- 析构" for the temporary. 用户说“没有体现”,可能用户运行了代码但没看到移动构造的输出?为什么? 在代码中,`s3` 的初始化行有注释:"// 构造+移动构造,优化后直接构造",意思禁用优化时应该有构造和移动构造。 但在用户的问题中,说“为什么没有体现引用延长生命周期”,可能用户误解了“延长生命周期”。 另一个点:在移动构造函数中,它调用 `swap(s)`,交换了内容。然后临时对象在析构时释放资源。但由于交换,原始资源被移动到新对象,所以临时对象只持有空指针或小资源。 代码中,移动构造函数是: ```cpp string(string&& s) { cout << "string(string&& s) -- 移动构造" << endl; swap(s); } ``` `swap(s)` 交换了 `this` 和 `s` 的内容。因为 `this` 在构造时是未初始化的,`swap` 会将 `s` 的成员交换给 `this`,并将 `this` 的初始成员(可能是垃圾)给 `s`。 在代码中,移动构造函数没有初始化列表,所以 `_str`, `_size`, `_capacity` 是未初始化的。然后调用 `swap(s)` ,它使用 `::swap` ,交换指针和大小。 在 `swap` 函数中: ```cpp void swap(string& s) { ::swap(_str, s._str); ::swap(_size, s._size); ::swap(_capacity, s._capacity); } ``` 所以,当移动构造时,临时对象 `s` 的成员被交换给新对象,新对象获得资源,临时对象获得新对象的初始状态。由于新对象是刚创建,它的 `_str` 是 nullptr(因为在类定义中 `char* _str = nullptr;`),所以当调用 `swap(s)` 后,新对象获得 `s` 的资源, `s` 获得 nullptr。 然后临时对象 `s` 在析构时, `delete[] nullptr;` 是安全的,因为 `delete[]` 处理 nullptr。 所以这应该工作正常。 回到用户的问题:为什么没有体现引用延长生命周期?可能用户期望临时对象的生命周期被延长,但实际上移动语义并不延长生命周期;它只是高效地转移资源。 或许用户在问为什么移动构造函数没有被调用,即使禁用了复制省略。 在代码中,对于 `s4`: ```cpp wusaqi::string s4 = move(s1); ``` 这明确使用 `move` 将 `s1` 转为,调用移动构造。 对于 `s3`,是临时对象,自动是。 在禁用复制省略后,输出应该显示移动构造。 但用户说“没有体现”,可能用户运行代码时,移动构造没有被输出?为什么? 检查代码的实现:在拷贝构造函数中,它也有输出 "拷⻉构造",但在 `s3` 初始化时,应该使用移动构造函数,因为临时对象是。 除非编译器没有选择移动构造函数。 在 `string` 类中,移动构造函数定义为 `string(string&& s)`,这应该匹配临时对象。 或许用户在编译时忘记了 `-fno-elide-constructors`?或者输出被缓冲了?但不太可能。 另一个想法:在代码中,`string` 类有拷贝赋和移动赋,但 `s3` 是初始化,不是赋,所以调用构造函数系列。 是的。 或许用户混淆了“延长生命周期”移动语义。 在C++中,当我们将临时对象绑定到 const 左引用时,临时对象的生命周期被延长到引用的生命周期。例如: ```cpp const string& ref = string("hello"); // 临时生命周期延长到ref的作用域 ``` 但在移动语义中,没有直接延长生命周期;移动构造函数接受引用,资源被移动,临时对象仍然在完整表达式结束时销毁。 在 `s3 = wusaqi::string("yyyyy");` 中,临时对象在表达式结束时销毁,但由于移动构造, `s3` 获得了资源。 所以,用户可能期望临时对象的生命周期被延长,但它没有。 用户的问题是:为什么没有体现引用延长生命周期?可能就是这个原因:引用本身并不延长生命周期;它允许移动资源。 在代码中,对于 `s3`,临时对象的生命周期很短,不是在移动后还活着。 但在移动构造函数中,通过交换,临时对象被留在一个安全状态,以便销毁。 所以,回答应该是:引用没有延长临时对象的生命周期;移动语义允许高效转移资源,但临时对象仍然在表达式结束时被销毁。禁用复制省略后,应该看到构造和移动构造的输出,表明临时对象被创建然后移动。 在输出中,对于 `s3`,应该看到: - "string(char* str)-构造" // for temporary - "string(string&& s) -- 移动构造" // for s3 - then soon after, "~string() -- 析构" // for temporary 而 `s3` 的析构在 main 结束时。 用户说“没有体现”,可能用户在运行代码时没有看到移动构造的输出?但根据代码,它应该输出。 除非在用户的实现中,编译器没有调用移动构造函数?但为什么? 在代码中,移动构造函数是可用的。 或许因为 `wusaqi::string("yyyyy")` 本身是临时对象,但移动构造应该被调用。 我认为用户可能误解了输出或术语。 回答时,我需要解释移动语义如何工作,并指出引用不延长生命周期。 最后,根据规则,我必须在回答后提出3-5个相关问题。 规则: - 使用中文回答问题 - 回答完问题后,提出3-5个 §§相关问题§§ - 第一行必须完整输出 "§§相关问题§§" ,禁止换行分散 - 相关问题基于用户的问题和我的回答 所以,结构: 1. 回答问题 2. 输出 "§§相关问题§§" 3. 列出相关问题,每个以数字点开头,如 "1. ..." 我的回答: 解释为什么没有体现生命周期延长:因为引用并不延长临时对象的生命周期;移动构造函数被调用以转移资源,但临时对象仍被销毁。使用 `-fno-elide-constructors` ,应该看到移动构造的输出。 然后相关问题,例如: 1. 在C++中,const左引用如何延长临时对象的生命周期? 2. 什么是复制省略,它在C++17中如何变化? 3. 移动构造函数如何处理资源管理以防止内存泄漏? 4. 为什么在移动构造函数中使用swap是常见的做法? 5. 在禁用优化后,代码输出应该是什么? </think> 您的代码实现了一个自定义的 `wusaqi::string` 类,并包含了移动构造函数和移动赋运算符来支持移动语义。问题聚焦于 `wusaqi::string s3 = wusaqi::string("yyyyy");` 这一行代码,在编译时添加 `-fno-elide-constructors` 选项(禁用复制省略)后,为什么“没有体现引用延长生命周期”。以下是详细解释: ### 原因分析: 1. **引用不直接延长生命周期**: -C++中,引用(如 `string&&`)主要用于实现移动语义,允许高效地“窃取”临时对象()的资源(如内存指针),避免深拷贝的开销。 - 然而,引用本身 **不会延长临时对象的生命周期**。临时对象(例如 `wusaqi::string("yyyyy")`)的生命周期在完整表达式结束时(即语句末尾)就会被销毁。移动构造函数只是将资源转移到新对象(`s3`),但临时对象本身依旧会被析构。 - 您提到的“延长生命周期”可能是对C++规则的误解。生命周期延长通常仅适用于 **const左引用**(如 `const string& ref = string("temp");`),此时临时对象的生命周期会延长至引用的作用域结束。但引用没有此特性。 2. **移动语义的行为**: - 在禁用复制省略(`-fno-elide-constructors`)后,`s3` 的初始化过程应该分为两步: - 第一步:构造临时对象 `wusaqi::string("yyyyy")`,调用普通构造函数(输出 `string(char* str)-构造`)。 - 第二步:临时对象作为,调用移动构造函数来初始化 `s3`(输出 `string(string&& s) -- 移动构造`)。 - 移动构造函数通过 `swap(s)` 交换资源,使 `s3` 获得临时对象的资源(如 `_str` 指针),而临时对象被留在一个安全状态(例如 `_str = nullptr`)。之后,临时对象在表达式结束时调用析构函数(输出 `~string() -- 析构`),但不会造成内存泄漏,因为资源已被转移。 3. **为什么“没有体现”生命周期延长**: - 如果您在输出中没有看到移动构造的调用(例如移动构造函数未输出消息),可能原因包括: - **编译器行为或输出缓冲问题**:代码中的 `cout` 输出可能被缓冲或未刷新,导致输出顺序混乱。建议检查运行环境(如IDE或终端)的日志是否完整捕获输出。 - **移动构造函数的触发条件**:`wusaqi::string("yyyyy")` 是一个纯(prvalue),应优先匹配移动构造函数而非拷贝构造函数。如果您的编译器严格遵循C++11标准,禁用优化后必须调用移动构造函数。 - **代码潜在问题**:在移动构造函数中,`swap(s)` 依赖于 `this` 对象的初始状态(`_str = nullptr` 来自类内初始化)。如果某些编译器在处理未初始化成员时未正确设置 `_str`,可能导致资源交换异常,但不会阻止移动构造的调用。 - 总之,移动语义“体现”在资源高效转移(避免拷贝),而非生命周期延长。临时对象仍被销毁,这符合标准行为。 ### 验证输出示例: 在禁用复制省略后,`main` 函数的输出应包含以下顺序(基于代码中的 `cout` 语句): - `string(char* str)-构造` // 临时对象 `wusaqi::string("yyyyy")` 构造 - `string(string&& s) -- 移动构造` // `s3` 通过移动构造初始化 - `~string() -- 析构` // 临时对象析构(资源已转移,安全销毁) - ...(后续其他对象的销毁) ### 建议: - 确保编译命令正确包含 `-fno-elide-constructors`(例如 `g++ -fno-elide-constructors main.cpp`)。 - 检查输出是否完整:如果移动构造函数消息缺失,可能是输出流问题(尝试添加 `cout.flush()` 调试)。
评论 1
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值