使用const引用延长变量生命周期

使用const引用延长变量生命周期

我们都知道const​ 引用在一定情况下会延长临时变量的生命周期,下面我们看一下具体哪些情况能够延长以及需要注意的地方。本节课不考虑编译器优化。

编译环境

Visual Studio 2022 / v143 / C++20

const引用一个右值

临时构造函数

#include <iostream>
class A
{
public:
    // 空参构造
    A()
    {
        std::cout << "A()" << std::endl;
    }
    // 带参构造
    A(const int v) : v(v)
    {
        std::cout << "A(const int v)" << std::endl;
    }
    // 拷贝构造
    A(const A&)
    {
        std::cout << "const A& " << std::endl;
    };
    // (左值)拷贝赋值
    A& operator=(const A&)
    {
        std::cout << "A& operator=(const A&) " << std::endl;
    }
    // 析构
    ~A()
    {
        std::cout << __FUNCTION__ << std::endl;
    }
    int v = 0;
};
class D
{
public:
    D(const A& a) :_a(a) {}
    ~D()
    {
        std::cout << __FUNCTION__ << std::endl;
    }
    const A& _a;
};

int main()
{
    A(); // 验证A()是临时变量,出作用域 分号 ; 即走完当前行便被析构
    {
        // 使用const引用延长它的生命
        const A& a = A();     // 这是个右值,必须用右值引用或常引用
        //A& a = A();         // 报错:   这是个右值,必须用右值引用或常引用
        //A&& a = A();        // ok
        //D d(a); // 延长了 c 的生命周期
        std::cout << "A.v: " << a.v << std::endl;
    }
    std::cout << "main return" << std::endl;
    return 0;
}

首先我们创建了一个临时变量A()​,然后使用const引用​延长他的生命周期。输出如下:

A()      // 临时 1
A::~A   
A()      // 被引用的临时2
A.v: 0   
A::~A
main return

函数参数

//A setA(A& a) // 非常量引用不能绑定右值
A setA(const A& a1)
{
    std::cout << &a << std::endl;
    return a1;
}
{
   const A& a2 = setA(A(2));
   std::cout << &a2 << std::endl;
} // 大括号

「输出」

A(const int v)
00AFFC88
const A&
A::~A  // 函数结束,a1被析构
00AFFD90
A::~A // a2 析构

首先创建了一个临时的A(2)​对象, 然后绑定到setA函数参数,A2的生命周期延长至函数体结束。在函数内将a拷贝一份到函数返回值,之后返回值被延长至大括号结束。

在vs中,此过程产生一次拷贝构造。

错误的使用-返回一个临时变量的引用

//A setA(A& a) // 非常量引用不能绑定右值
// 返回一个临时变量的引用, 未定义的行为
const A& setA(const A& a1)
{
    std::cout << &a1 << std::endl;
    return a1;
}
int main()
{
    const A& a2 = setA(A(2));
    std::cout << &a2 << std::endl;
    return 0;
}

「可能的输出」

A(const int v)
0062FB6C
A::~A  // 被析构
2
0062FB6C

上面结果大部分情况下运行正常,因为对象被析构不代表内存被清理,我们可以用调试器看一下。但是a2​引用了一个已经被析构的对象,不属于标准中定义的行为,且不容易被发现。类似的情况还有如下:

int n = 1;
auto p = std::minmax(n, n+1);
int m = p.first; // ok
int x = p.second; // 未定义行为
 
// 注意结构化绑定有同样的问题
auto [mm, xx] = std::minmax(n, n+1);
xx; // 未定义行为

实战-一个简单的日志输出

我们利用临时变量出作用域被析构、以及使用const引用能够延长其生命周期来实现一个简单的日志输出类。

#include <iostream>
#include <source_location>
#include <string>
#include <format>
#include <ranges>
// 
// main.cpp:12 message.

enum class LogLevel
{
    info = 0,
    error
};
// 抽象->
//class LogFormatter; // 输出的格式
//class LogAppender; // 输出的地方
class MyLogger
{
public:
    MyLogger(LogLevel level, const std::source_location& localtion = std::source_location::current()):
        _localtion(localtion),
        _level(level)
    { }
    // 析构的时候进行日志输出
    ~MyLogger()
    {
        std::cout << format() << std::endl;
    }

    const MyLogger& operator<<(const std::string& message) const
    {
        _message += message;
        return *this;
    }
private:
    std::string format()
    {
        std::string strLevel = "unknown";
        if (LogLevel::info == _level) {
            strLevel = "Info";
        }
        else if (LogLevel::error == _level) {
            strLevel = "Error";
        }
        return std::format("{} {}:{} {}", static_cast<int>(_level), _localtion.file_name(), _localtion.line(), _message);
    }
private:
    const std::source_location& _localtion;
    mutable std::string _message;
    LogLevel _level;
};

int main()
{
    {
        MyLogger(LogLevel::info) << "log message";
        // 延长生命周期
        const auto& logger = MyLogger(LogLevel::error);
        // 继续写入上一个日志
        for (auto i : std::views::iota(1, 10)) {

            logger << std::to_string(i) << " ";
        }
    }
    return 0;
}
posted @ 2023-10-03 19:31  天空之城00  阅读(474)  评论(0)    收藏  举报