首页 > 极客资料 博客日记

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

2024-09-12 15:30:02极客资料围观22

极客之家推荐掌握 C++17:结构化绑定与拷贝消除的妙用这篇文章给大家,欢迎收藏极客之家享受知识的乐趣

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




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

标签:

相关文章

本站推荐

标签云