当前位置: 首页 > news >正文

C++进阶技巧:如何在同一对象中存储左值或右值

一、背景

C++ 代码似乎经常出现一个问题:如果该值可以来自左值或右值,则对象如何跟踪该值?即如果保留该值作为引用,那么就无法绑定到临时对象。如果将其保留为一个值,那么当它从左值初始化时,会产生不必要的副本。

有几种方法可以应对这种情况。使用std::variant提供了一个很好的折衷方案来获得有表现力的代码。

二、跟踪值

假设有一个类MyClass。想让MyClass访问某个std::string。如何表示MyClass内部的字符串?有两种选择:

  • 将其存储为引用。

  • 将其存储为副本。

2.1、存储引用

如果将其存储为引用,例如const引用:

展开

代码语言:C++

自动换行

AI代码解释

class MyClass { public: explicit MyClass(std::string const& s) : s_(s) {} void print() const { std::cout << s_ << '\n'; } private: std::string const& s_; };

则可以用一个左值初始化我们的引用:

代码语言:C++

自动换行

AI代码解释

std::string s = "hello"; MyClass myObject{s}; myObject.print();

看起来很不错。但是,如果想用右值初始化我们的对象呢?例如:

代码语言:C++

自动换行

AI代码解释

MyClass myObject{std::string{"hello"}}; myObject.print();

或者这样的代码:

代码语言:C++

自动换行

AI代码解释

std::string getString(); // function declaration returning by value MyClass myObject{getString()}; myObject.print();

那么代码具有未定义的行为。原因是,临时字符串对象在创建它的同一条语句中被销毁。当调用print时,字符串已经被破坏,使用它是非法的,并导致未定义的行为。

为了说明这一点,如果将std::string替换为类型X,并且在X的析构函数打印日志:

展开

代码语言:C++

自动换行

AI代码解释

struct X { ~X() { std::cout << "X destroyed" << '\n';} }; class MyClass { public: explicit MyClass(X const& x) : x_(x) {} void print() const { // using x_; } private: X const& x_; };

在调用的地方也打印日志:

代码语言:C++

自动换行

AI代码解释

MyClass myObject(X{}); std::cout << "before print" << '\n'; myObject.print();

输出:

代码语言:Bash

自动换行

AI代码解释

X destroyed before print

可以看到,在尝试使用之前,这个X已经被破坏了。

完整示例:

展开

代码语言:C++

自动换行

AI代码解释

#include <iostream> #include <string> struct X { ~X() { std::cout << "X destroyed" << '\n';} }; class MyClass { public: explicit MyClass(X const& x) : x_(x) {} void print() { (void) x_; // using x_; } private: X const& x_; }; int main() { MyClass myObject(X{}); std::cout << "before print" << '\n'; myObject.print(); }

2.2、存储值

另一种选择是存储一个值。这允许使用move语义将传入的临时值移动到存储值中:

展开

代码语言:C++

自动换行

AI代码解释

class MyClass { public: explicit MyClass(std::string s) : s_(std::move(s)) {} void print() const { std::cout << s_ << '\n'; } private: std::string s_; };

现在调用它:

代码语言:C++

自动换行

AI代码解释

MyClass myObject{std::string{"hello"}}; myObject.print();

产生两次移动(一次构造s,一次构造s_),并且没有未定义的行为。实际上,即使临时对象被销毁,print也会使用类内部的实例。

不幸的是,如果带着左值返回到第一个调用点:

代码语言:C++

自动换行

AI代码解释

std::string s = "hello"; MyClass myObject{s}; myObject.print();

那么就不再做两次移动了:做了一次复制(构造s)和一次移动(构造s_)。

更重要的是,我们的目的是给MyClass访问字符串的权限,如果做一个拷贝,就有了一个不同于进来的实例。所以它们不会同步。

对于临时对象来说,这不是问题,因为它无论如何都会被销毁,并且我们在之前将它移了进来,所以仍然可以访问字符串。但是通过复制,我们不再给MyClass访问传入字符串的权限。

所以存储一个值也不是一个好的解决方案。

三、存储variant

存储引用不是一个好的解决方案,存储值也不是一个好的解决方案。我们想做的是,如果引用是从左值初始化的,则存储引用;如果引用是从右值初始化的,则存储引用。

但是数据成员只能是一种类型:值或引用,对吗?

但是,对于std::variant,它可以是任意一个。不过,如果尝试在一个变量中存储引用,就像这样:

代码语言:C++

自动换行

AI代码解释

std::variant<std::string, std::string const&>

将得到一个编译错误:

代码语言:C++

自动换行

AI代码解释

variant must have no reference alternative

为了达到我们的目的,需要将引用放在另一个类型中;即必须编写特定的代码来处理数据成员。如果为std::string编写这样的代码,则不能将其用于其他类型。

在这一点上,最好以通用的方式编写代码。

四、通用存储类

存储需要是一个值或一个引用。既然现在是为通用目的编写这段代码,那么也可以允许非const引用。由于变量不能直接保存引用,那么可以将它们存储到包装器中:

展开

代码语言:C++

自动换行

AI代码解释

template<typename T> struct NonConstReference { T& value_; explicit NonConstReference(T& value) : value_(value){}; }; template<typename T> struct ConstReference { T const& value_; explicit ConstReference(T const& value) : value_(value){}; }; template<typename T> struct Value { T value_; explicit Value(T&& value) : value_(std::move(value)) {} };

将存储定义为这两种情况之一:

代码语言:C++

自动换行

AI代码解释

template<typename T> using Storage = std::variant<Value<T>, ConstReference<T>, NonConstReference<T>>;

现在需要通过提供引用来访问变量的底层值。创建了两种类型的访问:一种是const,另一种是非const

4.1、定义const访问

要定义const访问,需要使变量内部的三种可能类型中的每一种都产生一个const引用。

为了访问变量中的数据,将使用std::visit和规范的overload模式,这可以在c++ 17中实现:

代码语言:C++

自动换行

AI代码解释

template<typename... Functions> struct overload : Functions... { using Functions::operator()...; overload(Functions... functions) : Functions(functions)... {} };

要获得const引用,只需为每种variant创建一个:

展开

代码语言:C++

自动换行

AI代码解释

template<typename T> T const& getConstReference(Storage<T> const& storage) { return std::visit( overload( [](Value<T> const& value) -> T const& { return value.value_; }, [](NonConstReference<T> const& value) -> T const& { return value.value_; }, [](ConstReference<T> const& value) -> T const& { return value.value_; } ), storage ); }

4.2、定义非const访问

非const引用的创建使用相同的技术,除了variantConstReference之外,它不能产生非const引用。然而,当std::visit访问一个变量时,必须为它的每一个可能的类型编写代码:

展开

代码语言:C++

自动换行

AI代码解释

template<typename T> T& getReference(Storage<T>& storage) { return std::visit( overload( [](Value<T>& value) -> T& { return value.value_; }, [](NonConstReference<T>& value) -> T& { return value.value_; }, [](ConstReference<T>& ) -> T&. { /* code handling the error! */ } ), storage ); }

进一步优化,抛出一个异常:

展开

代码语言:C++

自动换行

AI代码解释

struct NonConstReferenceFromReference : public std::runtime_error { explicit NonConstReferenceFromReference(std::string const& what) : std::runtime_error{what} {} }; template<typename T> T& getReference(Storage<T>& storage) { return std::visit( overload( [](Value<T>& value) -> T& { return value.value_; }, [](NonConstReference<T>& value) -> T& { return value.value_; }, [](ConstReference<T>& ) -> T& { throw NonConstReferenceFromReference{"Cannot get a non const reference from a const reference"} ; } ), storage ); }

五、创建存储

已经定义了存储类,可以在示例中使用它来访问传入的std::string,而不管它的值类别:

展开

代码语言:C++

自动换行

AI代码解释

class MyClass { public: explicit MyClass(std::string& value) : storage_(NonConstReference(value)){} explicit MyClass(std::string const& value) : storage_(ConstReference(value)){} explicit MyClass(std::string&& value) : storage_(Value(std::move(value))){} void print() const { std::cout << getConstReference(storage_) << '\n'; } private: Storage<std::string> storage_; };

(1)调用时带左值:

代码语言:C++

自动换行

AI代码解释

std::string s = "hello"; MyClass myObject{s}; myObject.print();

匹配第一个构造函数,并在存储成员内部创建一个NonConstReference。当print函数调用getConstReference时,非const引用被转换为const引用。

(2)使用临时值:

代码语言:C++

自动换行

AI代码解释

MyClass myObject{std::string{"hello"}}; myObject.print();

这个函数匹配第三个构造函数,并将值移动到存储中。getConstReference然后将该值的const引用返回给print函数。

http://www.jsqmd.com/news/84585/

相关文章:

  • 进程PCB
  • 豆包手机AI Agent技术深度解析
  • Springboot医药采购管理2mqc3(程序+源码+数据库+调试部署+开发环境)带论文文档1万字以上,文末可获取,系统界面在最后面。
  • LeetCode 41. 缺失的第一个正数 | 原地哈希最优解全解析
  • 线性回归模型
  • 榛子矮砧密植:水肥一体化系统的铺设要点指南
  • 我在私有漏洞赏金计划常规测试中发现IP欺骗漏洞的过程
  • 基于无迹卡尔曼滤波(UKF)与模型预测控制(MPC)的多无人机避撞研究附Matlab代码
  • java基础流程控制笔记
  • Flutter 通用轮播图组件 BannerWidget:自动播放 + 指示器 + 全场景适配
  • Java冷启动全指南:从原理到实战优化
  • 校招 Java 面试必看:JVM 其实就考这 3 个点(我帮你讲透)
  • 列表基本概念
  • Flutter 通用下拉刷新上拉加载列表 RefreshListWidget:分页 + 空态 + 错误处理
  • 【Java方法】--递归的正确使用方法,告别栈溢出
  • 【JavaWeb】Servlet继承结构
  • Linux网络编程-udp
  • [从零构建操作系统]08 函数调用时栈的底层行为解析
  • Springboot医疗云胶片管理系统nem7x(程序+源码+数据库+调试部署+开发环境)带论文文档1万字以上,文末可获取,系统界面在最后面。
  • MATLAB与FlightGear联合仿真教程:包含Simulink工程文件的PDF指南
  • 实战教程:1小时掌握逆向Unity游戏 (共13课时)
  • 提升SEO效率:2025年真正有效的8款AI工具终极清单
  • Day 37 MLP神经网络的训练
  • 力扣hot100:搜索插入位置
  • 探索含光伏、火电与飞轮储能系统的奇妙调频之旅
  • 高效获取高质量外链:2026年必须掌握的10个核心策略
  • Flutter国际化(i18n)实现详解
  • 【高可用系统监控的设计原则与实践】
  • 基于 STM32 的太阳能 MPPT 充电控制器设计
  • 30分钟掌握Semgrep:代码安全检查从入门到精通