news 2026/6/10 15:38:57

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

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
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函数。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/6/10 13:36:03

基于文化优化算法图像量化附Matlab代码

✅作者简介&#xff1a;热爱科研的Matlab仿真开发者&#xff0c;擅长数据处理、建模仿真、程序设计、完整代码获取、论文复现及科研仿真。&#x1f34e; 往期回顾关注个人主页&#xff1a;Matlab科研工作室&#x1f34a;个人信条&#xff1a;格物致知,完整Matlab代码及仿真咨询…

作者头像 李华
网站建设 2026/6/10 13:33:51

基于自抗扰控制ADRC的永磁同步电机仿真模型附Simulink仿真

✅作者简介&#xff1a;热爱科研的Matlab仿真开发者&#xff0c;擅长数据处理、建模仿真、程序设计、完整代码获取、论文复现及科研仿真。&#x1f34e; 往期回顾关注个人主页&#xff1a;Matlab科研工作室&#x1f34a;个人信条&#xff1a;格物致知,完整Matlab代码及仿真咨询…

作者头像 李华
网站建设 2026/6/10 13:34:12

Linux网络编程-udp

1.今天的内容包括&#xff1a;udp通信的编程方法、广播通信的方法2.udp通信udp和tcp通信方式2.1socket创建使用SOCK_DGRAM创建。2.2发送和接收数据使用sendto和recvfrom&#xff0c;因为没有建立连接所以每次都要有ip和port&#xff0c;就是使用struct sockaddr地址。都是六个参…

作者头像 李华
网站建设 2026/6/10 0:35:24

LLC谐振变换器恒压恒流双竞争闭环Simulink仿真探索

LLC谐振变换器恒压恒流双竞争闭环simulink仿真&#xff08;附说明文档&#xff09; 1.采用电压电流双环竞争控制&#xff08;恒压恒流&#xff09; 2.附双环竞争仿真文件&#xff08;内含仿真介绍&#xff0c;波形分析&#xff0c;增益曲线计算.m代码&#xff09; 仿真参数&…

作者头像 李华
网站建设 2026/6/10 15:13:37

【Java方法】--递归的正确使用方法,告别栈溢出

个人主页 目录前言&#x1f4a1;1.什么是递归&#xff1f;1.1 递归的两个关键要素1.2 递归结构&#xff1a;2.经典的递归2.1 案例一&#xff1a;阶乘计算2.2 案例二&#xff1a;斐波那契数列2.3 目录遍历3.深入理解递归为什么会栈溢出3.1 什么是栈&#xff1f;Java 虚拟机栈结合…

作者头像 李华
网站建设 2026/6/10 15:15:31

视觉色选机:如何挑选技术可靠与服务完善的设备厂家

现今&#xff0c;于粮食加工行业里&#xff0c;视觉色选机成了保障产品品质的关键设备&#xff0c;它能提升附加值&#xff0c;还能实现自动化生产。它借助高分辨率相机捕捉物料图像&#xff0c;运用智能算法实时识别颜色&#xff0c;识别形状&#xff0c;识别内部缺陷&#xf…

作者头像 李华