首页 > 编程语言 >掌握 C++17:结构化绑定与拷贝消除的妙用

掌握 C++17:结构化绑定与拷贝消除的妙用

时间:2024-09-12 15:15:47浏览次数:10  
标签:std 妙用 17 tuple 绑定 C++ MyClass 拷贝 include

C++17 特性示例

1. 结构化绑定(Structured Binding)

结构化绑定允许你用一个对象的元素或成员同时实例化多个实体。
结构化绑定允许你在声明变量的同时解构一个复合类型的数据结构(如 结构体,std::tuplestd::pair, 或者 std::array)。这样可以方便地获取多个值,而不需要显式地调用 std::tie() 或者 .get() 方法。

使用结构化绑定,能大大提升代码的可读性:

#include <iostream>
#include <string>
#include <unordered_map>

int main() {

  std::unordered_map<std::string,std::string> mymap;
  mymap.emplace("k1","v1");
  mymap.emplace("k2","v2");
  mymap.emplace("k2","v3");

  for (const auto& elem : mymap)
      std::cout << "old: " << elem.first << " : " << elem.second << std::endl;

  for (const auto& [key,value] : mymap)
      std::cout << " new: " << "key: " << key << ", value: " << value  << std::endl;

  return 0;
}

### 细说结构化绑定
为了理解结构化绑定,必须意识到这里面其实有一个隐藏的匿名对象。结构化绑定时引入的新变量名其实都指向这个匿名对象的成员/元素。
绑定到一个匿名实体
如下初始化的精确行为:
struct MyStruct {
  int i = 0;
  std::string s;
};
MyStruct ms;
auto [u, v] = ms;
等价于我们用 ms初始化了一个新的实体 e,并且让结构化绑定中的 u和 v变成 e的成员的别名,类似于如下定义:
auto e = ms;
aliasname u = e.i;
aliasname v = e.s;
这意味着 u和 v仅仅是 ms的一份本地拷贝的成员的别名。然而,我们没有为 e声明一个名称,因此我们不能直接访问这个匿名对象。注意 u和 v并不是 e.i和 e.s的引用(而是它们的别名)。decltype(u)的结果是成员 i的类型,declytpe(v)的结果是成员 s的类型。因此:
std::cout << u << ' ' << v << '\n';
会打印出 e.i和 e.s(分别是 ms.i和 ms.s的拷贝)。

e的生命周期和结构化绑定的生命周期相同,当结构化绑定离开作用域时 e也会被自动销毁。另外,除非使用了引用,否则修改用于初始化的变量并不会影响结构化绑定引入的变量(反过来也一样)  

### 示例代码
```cpp
#include <iostream>
#include <tuple>
#include <vector>
#include <string>
#include <map>
#include <unordered_map>

struct MyStruct {
      int num {1};
      std::string str {"test"};
};

int main() {

  // 使用结构化绑定从 tuple 中提取值
  std::tuple<int, double, std::string> data = std::make_tuple(1, 3.14, "hello");
  auto [a, b, c] = data;
  std::cout << "a: " << a << ", b: " << b << ", c: " << c << std::endl;


  //value
  MyStruct my_struc;
  auto [num,str] = my_struc;
  std::cout << "num: " << num << ", str: " << str  << std::endl;
  my_struc.num = 2;
  str = "test2";
  std::cout << "num: " << num << ", str: " << str  << std::endl;


  //ref
  MyStruct my_struc2 {2,"test2"};
  const auto& [num2,str2] = my_struc2;
  std::cout << "num2: " << num2 << ", str2: " << str2  << std::endl;
  my_struc2.num = 4;
  std::cout << "num2: " << num2 << ", str2: " << str2  << std::endl;

  return 0;
}

### 总结
理论上讲,结构化绑定适用于任何有 public数据成员的结构体、C 风格数组和“类似元组 (tuple-like)的对
象”:
• 对于所有非静态数据成员都是 public的结构体和类,你可以把每一个成员绑定到一个新的变量名上。
• 对于原生数组,你可以把数组的每一个元素都绑定到新的变量名上。
• 对于任何类型,你可以使用 tuple-like API 来绑定新的名称,无论这套 API 是如何定义“元素”的。对于一个
类型 type这套 API 需要如下的组件:
– std::tuple_size<type>::value要返回元素的数量。
– std::tuple_element<idx, type>::type 要返回第 idx个元素的类型。
– 一个全局或成员函数 get<idx>()要返回第 idx个元素的值。
标准库类型 std::pair<>、std::tuple<>、std::array<> 就是提供了这些 API 的例子。如果结构体和类提供了 tuple-like API,那么将会使用这些 API 进行绑定,而不是直接绑定数据成员。

## 2. 拷贝消除(Copy Elision)

从技术上讲,C++17引入了一个新的规则:当以值传递或返回一个临时对象的时候必须省略对该临时对象的拷贝。
从效果上讲,我们实际上是传递了一个未实质化的对象 (unmaterialized object)。

自从第一次标准开始,C++就允许在某些情况下省略 (elision)拷贝操作,即使这么做可能会影响程序的运行结果(例如,拷贝构造函数里的一条打印语句可能不会再执行)。当用临时对象初始化一个新对象时就很容易出现这种情况,尤其是当一个函数以值传递或返回临时对象的时候。例如:
```cpp
#include <iostream>
#include <tuple>
#include <vector>
#include <string>
#include <map>
#include <complex>


class MyClass
{
public:
  // 没有拷贝/移动构造函数的定义
  MyClass(const MyClass&) = delete;
  MyClass(MyClass&&) = delete;
};

MyClass bar() {
  return MyClass{};       // 返回临时对象
}

int main() {
  MyClass x = bar();  // 使用返回的临时对象初始化x

  return 0;
}
上面的代码用c++14会报错,c++17已经不报错了

然而,注意其他可选的省略拷贝的场景仍然是可选的,这些场景中仍然需要一个拷贝或者移动构造函数。例如:
MyClass foo()
{
  MyClass obj;
  ...
  return obj; // 仍 然 需 要 拷 贝/移 动 构 造 函 数 的 支 持
}
这里,foo()中有一个具名的变量 obj (当使用它时它是左值 (lvalue))。因此,具名返回值优化 (named returnvalue optimization)(NRVO) 会生效,然而该优化仍然需要拷贝/移动支持。当 obj是形参的时候也会出现这种情况:
MyClass bar(MyClass obj) // 传 递 临 时 变 量 时 会 省 略 拷 贝
{
  ...
  return obj; // 仍 然 需 要 拷 贝/移 动 支 持
}

当传递一个临时变量(也就是纯右值 (prvalue))作为实参时不再需要拷贝/移动,但如果返回这个参数的话仍然需要拷贝/ 移动支持因为返回的对象是具名的。 

### 作用
这个特性的一个显而易见的作用就是减少拷贝会带来更好的性能。尽管很多主流编译器之前就已经进行了这种优化,但现在这一行为有了标准的保证。尽管移动语义能显著的减少拷贝开销,但如果直接不拷贝还是能带来很大的性能提升(例如当对象有很多基本类型成员时移动语义还是要拷贝每个成员)。另外这个特性可以减少输出参数的使用,转而直接返回一个值(前提是这个值直接在返回语句里创建)。另一个作用是可以定义一个总是可以工作的工厂函数,因为现在它甚至可以返回不允许拷贝或移动的对象。
例如,考虑如下泛型工厂函数:
```cpp
#include <iostream>
#include <tuple>
#include <vector>
#include <string>
#include <map>
#include <complex>
#include <utility>
#include <memory>
#include <atomic>


template <typename T, typename... Args>
T create(Args&&... args)
{
  return T{std::forward<Args>(args)...};
}


int main() {
  int i = create<int>(42);
  std::unique_ptr<int> up = create<std::unique_ptr<int>>(new int{42});
  std::atomic<int> ai = create<std::atomic<int>>(42);
  std::cout << "ai: " << ai << std::endl;
  return 0;
}



标签:std,妙用,17,tuple,绑定,C++,MyClass,拷贝,include
From: https://www.cnblogs.com/liudw-0215/p/18410226

相关文章

  • C++知识点:size_t, a.at(i), reverse函数
    1.size_t`size_t`是一种在C/C++编程中非常常用的数据类型,它定义在`<stddef.h>`或者`<cstdlib>`等头文件中,通常用来表示**大小**或**长度**。###关键特性:1.**无符号类型**:`size_t`是无符号整数类型,表示它只能存储非负整数。因此,它不会用于存储负值,这使得它非常适合表示诸如......
  • C++ 指针
    声明int*ipl,*ip2;//ipl和ip2都是指向int型对象的指针doubledp,*dp2;//dp2是指向double型对象的指针,dp是double型对象因为引用不是对象,没有实际地址,所以不能定义指向引用的指针。指针值指针的值(即地址)应属下列4种状态之一:指向一个对象。指向紧邻对象所占空......
  • C++ 声明和定义
    变量声明规定了变量的类型和名字,在这一点上定义与之相同。但是除此之外,定义还申请存储空间,也可能会为变量赋一个初始值。如果想声明一个变量而非定义它,就在变量名前添加关键字extern,而且不要显式地初始化变量:externinti;//声明i而非定义iintj;//声明并定义了任何包......
  • C++ 变量初始化
    列表初始化当用于内置类型的变量时,这种初始化形式有一个重要特点:如果我们使用列表初始化且初始值存在丢失信息的风险,则编译器将报错:longdoubleld=3.1415926536;inta{ld},b={ld};//错误:转换未执行,因为存在丢失信息的危险intc(ld),d=ld;//正确:转换执行,且确实丢失了......
  • C++复习day11
    类型转化C语言中的类型转换在C语言中,如果赋值运算符左右两侧类型不同,或者形参与实参类型不匹配,或者返回值类型与接收返回值类型不一致时,就需要发生类型转化,C语言中总共有两种形式的类型转换:隐式类型转换和显式类型转换。隐式类型转化:编译器在编译阶段自动进行,能转就转,......
  • C++竞赛初阶L1-15-第六单元-多维数组(34~35课)556: T456506 矩阵转置
    题目内容输入一个 n 行 m 列的矩阵 A,输出它的转置 AT。输入格式第一行包含两个整数 n 和 m,表示矩阵 A 的行数和列数。1≤n≤100,1≤m≤100。接下来 n 行,每行 m 个整数,表示矩阵 A 的元素。相邻两个整数之间用单个空格隔开,每个元素均在 1∼1000 之间。输......
  • C++竞赛初阶L1-15-第六单元-多维数组(34~35课)557: T456507 图像旋转
    题目内容输入一个 n 行 m 列的黑白图像,将它顺时针旋转 90 度后输出。输入格式第一行包含两个整数 n 和 m,表示图像包含像素点的行数和列数。1≤n≤100,1≤m≤100。接下来 n 行,每行 m 个整数,表示图像的每个像素点灰度。相邻两个整数之间用单个空格隔开,每个元素......
  • C++竞赛初阶L1-15-第六单元-多维数组(34~35课)555: T456505 矩阵乘法
    题目内容计算两个矩阵的乘法。n×m 阶的矩阵 A 乘以 m×k 阶的矩阵 B 得到的矩阵 C 是 n×k 阶的,且 C[i][j]=A[i][0]×B[0][j]+A[i][1]×B[1][j]+ …… +A[i][m−1]×B[m−1][j](C[i][j] 表示 C 矩阵中第 i 行第 j 列元素)。输入格式第一行为 n,m,k,表......
  • C++竞赛初阶L1-15-第六单元-多维数组(34~35课)554: T456504 矩阵加法
    题目内容输入两个 n 行 m 列的矩阵 A 和 B,输出它们的和 A+B,矩阵加法的规则是两个矩阵中对应位置的值进行加和,具体参照样例。输入格式第一行包含两个整数 n 和 m,表示矩阵的行数和列数 (1≤n≤100,1≤m≤100)。接下来 n 行,每行 m 个整数,表示矩阵 A 的元素......
  • Python习题 177:设计银行账户类并实现存取款功能
    (编码题)Python实现一个简单的银行账户类BankAccount,包含初始化方法、存款、取款、获取余额等功能。参考答案分析需求如下。Python类BankAccount,用于模拟银行账户的基本功能。该类应包含以下方法:初始化方法:接受两个参数:account_holder(账户持有人的姓名)和balance(账户......