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

2024-04-16 14:44

本文主要是介绍C++进阶技巧:如何在同一对象中存储左值或右值,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

目录

1.背景

2.跟踪值

2.1.存储引用

2.2.存储值

3.存储variant

4.通用存储类

4.1.定义const访问

4.2.定义非const访问

5.创建存储

6.总结


1.背景

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

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

2.跟踪值

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

  • 将其存储为引用。

  • 将其存储为副本。

2.1.存储引用

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

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

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

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

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

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

或者这样的代码:

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

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

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

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_;
};

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

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

输出:

X destroyed
before print

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

完整示例:

#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语义将传入的临时值移动到存储值中:

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

现在调用它:

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

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

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

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

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

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

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

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

3.存储variant

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

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

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

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

将得到一个编译错误:

variant must have no reference alternative

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

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

4.通用存储类

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

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)) {}
};

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

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中实现:

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

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

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访问一个变量时,必须为它的每一个可能的类型编写代码:

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);
}

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

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);
}

5.创建存储

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

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)调用时带左值:

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

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

(2)使用临时值:

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

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

6.总结

  std::variant为c++中跟踪左值或右值的经典问题提供了一种非常适合的解决方案。这种技术的代码具有表现力,因为std::variant允许表达与我们的意图非常接近的东西:“根据上下文,对象可以是引用或值”。

        在C++ 17和std::variant之前,解决这个问题很棘手,导致代码难以正确编写。随着语言的发展,标准库变得越来越强大,可以用越来越多的表达性代码来表达我们的意图。

这篇关于C++进阶技巧:如何在同一对象中存储左值或右值的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!


原文地址:
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.chinasem.cn/article/909084

相关文章

c++日志库log4cplus快速入门小结

《c++日志库log4cplus快速入门小结》文章浏览阅读1.1w次,点赞9次,收藏44次。本文介绍Log4cplus,一种适用于C++的线程安全日志记录API,提供灵活的日志管理和配置控制。文章涵盖... 目录简介日志等级配置文件使用关于初始化使用示例总结参考资料简介log4j 用于Java,log4c

C++归并排序代码实现示例代码

《C++归并排序代码实现示例代码》归并排序将待排序数组分成两个子数组,分别对这两个子数组进行排序,然后将排序好的子数组合并,得到排序后的数组,:本文主要介绍C++归并排序代码实现的相关资料,需要的... 目录1 算法核心思想2 代码实现3 算法时间复杂度1 算法核心思想归并排序是一种高效的排序方式,需要用

Go语言网络故障诊断与调试技巧

《Go语言网络故障诊断与调试技巧》在分布式系统和微服务架构的浪潮中,网络编程成为系统性能和可靠性的核心支柱,从高并发的API服务到实时通信应用,网络的稳定性直接影响用户体验,本文面向熟悉Go基本语法和... 目录1. 引言2. Go 语言网络编程的优势与特色2.1 简洁高效的标准库2.2 强大的并发模型2.

Python 函数详解:从基础语法到高级使用技巧

《Python函数详解:从基础语法到高级使用技巧》本文基于实例代码,全面讲解Python函数的定义、参数传递、变量作用域及类型标注等知识点,帮助初学者快速掌握函数的使用技巧,感兴趣的朋友跟随小编一起... 目录一、函数的基本概念与作用二、函数的定义与调用1. 无参函数2. 带参函数3. 带返回值的函数4.

基于Python实现进阶版PDF合并/拆分工具

《基于Python实现进阶版PDF合并/拆分工具》在数字化时代,PDF文件已成为日常工作和学习中不可或缺的一部分,本文将详细介绍一款简单易用的PDF工具,帮助用户轻松完成PDF文件的合并与拆分操作... 目录工具概述环境准备界面说明合并PDF文件拆分PDF文件高级技巧常见问题完整源代码总结在数字化时代,PD

javaSE类和对象进阶用法举例详解

《javaSE类和对象进阶用法举例详解》JavaSE的面向对象编程是软件开发中的基石,它通过类和对象的概念,实现了代码的模块化、可复用性和灵活性,:本文主要介绍javaSE类和对象进阶用法的相关资... 目录前言一、封装1.访问限定符2.包2.1包的概念2.2导入包2.3自定义包2.4常见的包二、stati

C++11范围for初始化列表auto decltype详解

《C++11范围for初始化列表autodecltype详解》C++11引入auto类型推导、decltype类型推断、统一列表初始化、范围for循环及智能指针,提升代码简洁性、类型安全与资源管理效... 目录C++11新特性1. 自动类型推导auto1.1 基本语法2. decltype3. 列表初始化3

C++11右值引用与Lambda表达式的使用

《C++11右值引用与Lambda表达式的使用》C++11引入右值引用,实现移动语义提升性能,支持资源转移与完美转发;同时引入Lambda表达式,简化匿名函数定义,通过捕获列表和参数列表灵活处理变量... 目录C++11新特性右值引用和移动语义左值 / 右值常见的左值和右值移动语义移动构造函数移动复制运算符

C语言进阶(预处理命令详解)

《C语言进阶(预处理命令详解)》文章讲解了宏定义规范、头文件包含方式及条件编译应用,强调带参宏需加括号避免计算错误,头文件应声明函数原型以便主函数调用,条件编译通过宏定义控制代码编译,适用于测试与模块... 目录1.宏定义1.1不带参宏1.2带参宏2.头文件的包含2.1头文件中的内容2.2工程结构3.条件编

游戏闪退弹窗提示找不到storm.dll文件怎么办? Stormdll文件损坏修复技巧

《游戏闪退弹窗提示找不到storm.dll文件怎么办?Stormdll文件损坏修复技巧》DLL文件丢失或损坏会导致软件无法正常运行,例如我们在电脑上运行软件或游戏时会得到以下提示:storm.dll... 很多玩家在打开游戏时,突然弹出“找不到storm.dll文件”的提示框,随后游戏直接闪退,这通常是由于