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

相关文章

Ilya-AI分享的他在OpenAI学习到的15个提示工程技巧

Ilya(不是本人,claude AI)在社交媒体上分享了他在OpenAI学习到的15个Prompt撰写技巧。 以下是详细的内容: 提示精确化:在编写提示时,力求表达清晰准确。清楚地阐述任务需求和概念定义至关重要。例:不用"分析文本",而用"判断这段话的情感倾向:积极、消极还是中性"。 快速迭代:善于快速连续调整提示。熟练的提示工程师能够灵活地进行多轮优化。例:从"总结文章"到"用

Spring Security 从入门到进阶系列教程

Spring Security 入门系列 《保护 Web 应用的安全》 《Spring-Security-入门(一):登录与退出》 《Spring-Security-入门(二):基于数据库验证》 《Spring-Security-入门(三):密码加密》 《Spring-Security-入门(四):自定义-Filter》 《Spring-Security-入门(五):在 Sprin

异构存储(冷热数据分离)

异构存储主要解决不同的数据,存储在不同类型的硬盘中,达到最佳性能的问题。 异构存储Shell操作 (1)查看当前有哪些存储策略可以用 [lytfly@hadoop102 hadoop-3.1.4]$ hdfs storagepolicies -listPolicies (2)为指定路径(数据存储目录)设置指定的存储策略 hdfs storagepolicies -setStoragePo

HDFS—存储优化(纠删码)

纠删码原理 HDFS 默认情况下,一个文件有3个副本,这样提高了数据的可靠性,但也带来了2倍的冗余开销。 Hadoop3.x 引入了纠删码,采用计算的方式,可以节省约50%左右的存储空间。 此种方式节约了空间,但是会增加 cpu 的计算。 纠删码策略是给具体一个路径设置。所有往此路径下存储的文件,都会执行此策略。 默认只开启对 RS-6-3-1024k

Java进阶13讲__第12讲_1/2

多线程、线程池 1.  线程概念 1.1  什么是线程 1.2  线程的好处 2.   创建线程的三种方式 注意事项 2.1  继承Thread类 2.1.1 认识  2.1.2  编码实现  package cn.hdc.oop10.Thread;import org.slf4j.Logger;import org.slf4j.LoggerFactory

【C++ Primer Plus习题】13.4

大家好,这里是国中之林! ❥前些天发现了一个巨牛的人工智能学习网站,通俗易懂,风趣幽默,忍不住分享一下给大家。点击跳转到网站。有兴趣的可以点点进去看看← 问题: 解答: main.cpp #include <iostream>#include "port.h"int main() {Port p1;Port p2("Abc", "Bcc", 30);std::cout <<

C++包装器

包装器 在 C++ 中,“包装器”通常指的是一种设计模式或编程技巧,用于封装其他代码或对象,使其更易于使用、管理或扩展。包装器的概念在编程中非常普遍,可以用于函数、类、库等多个方面。下面是几个常见的 “包装器” 类型: 1. 函数包装器 函数包装器用于封装一个或多个函数,使其接口更统一或更便于调用。例如,std::function 是一个通用的函数包装器,它可以存储任意可调用对象(函数、函数

C++11第三弹:lambda表达式 | 新的类功能 | 模板的可变参数

🌈个人主页: 南桥几晴秋 🌈C++专栏: 南桥谈C++ 🌈C语言专栏: C语言学习系列 🌈Linux学习专栏: 南桥谈Linux 🌈数据结构学习专栏: 数据结构杂谈 🌈数据库学习专栏: 南桥谈MySQL 🌈Qt学习专栏: 南桥谈Qt 🌈菜鸡代码练习: 练习随想记录 🌈git学习: 南桥谈Git 🌈🌈🌈🌈🌈🌈🌈🌈🌈🌈🌈🌈🌈�

【C++】_list常用方法解析及模拟实现

相信自己的力量,只要对自己始终保持信心,尽自己最大努力去完成任何事,就算事情最终结果是失败了,努力了也不留遗憾。💓💓💓 目录   ✨说在前面 🍋知识点一:什么是list? •🌰1.list的定义 •🌰2.list的基本特性 •🌰3.常用接口介绍 🍋知识点二:list常用接口 •🌰1.默认成员函数 🔥构造函数(⭐) 🔥析构函数 •🌰2.list对象

06 C++Lambda表达式

lambda表达式的定义 没有显式模版形参的lambda表达式 [捕获] 前属性 (形参列表) 说明符 异常 后属性 尾随类型 约束 {函数体} 有显式模版形参的lambda表达式 [捕获] <模版形参> 模版约束 前属性 (形参列表) 说明符 异常 后属性 尾随类型 约束 {函数体} 含义 捕获:包含零个或者多个捕获符的逗号分隔列表 模板形参:用于泛型lambda提供个模板形参的名