前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >【C++新特性】C++17结构化绑定

【C++新特性】C++17结构化绑定

作者头像
公众号guangcity
发布2020-10-30 12:12:01
6.6K0
发布2020-10-30 12:12:01
举报
文章被收录于专栏:光城(guangcity)光城(guangcity)
  • 1.1 更详细的阐述结构化绑定
    • 1.1.1 绑定到匿名对象
    • 1.1.2 使用修饰符
    • 1.1.3 修饰符并非修饰结构化绑定
    • 1.1.4 移动语义
  • 1.2 结构化绑定可以在哪使用
    • 1.2.1 结构体和类
    • 1.2.2 原生数组
    • 1.2.3 std::pair,std::tuple和std::array
  • 1.3 为结构化绑定提供类似tuple的API
    • 1.3.1 只读结构化绑定
    • 1.3.2 结构化绑定写操作

1.结构化绑定

结构化绑定:通过对象的元素或成员初始化多个实体。

一个结构体如下:

代码语言:javascript
复制
struct MyStruct {
  int i = 0;
  std::string s;
};

我们可以将一个MyStruct对象绑定到两个变量上。

代码语言:javascript
复制
MyStruct ms;
auto [u, v] = ms;

在这里,u和v就是所谓的结构化绑定。将结构体的成员分解初始化了u、v变量。

结构化绑定对于返回结构或数组的函数特别有用。

例如:返回一个结构体函数

代码语言:javascript
复制
MyStruct getStruct() {
  return MyStruct{42, "hello"};
};

此时我们可以直接通过结构化绑定拿到结构体的成员。

代码语言:javascript
复制
auto [id, val] = getStruct();

另一个比较有意思的使用地方在于可以增加代码可读性,例如输出map中所有的键值对。map如下:

代码语言:javascript
复制
std::map<int, std::string> mymap = {{1, "el"}, {3, "tom"}, {4, "nic"}};

结构化绑定之前我们遍历给定的是无意义的elem。

代码语言:javascript
复制
for (const auto& elem : mymap) {
  std::cout << elem.first << ": "  << elem.second << std::endl;
}

有了结构体绑定之后,我们只需要[key, val]

代码语言:javascript
复制
for (const auto& [key, val] : mymap) {
  std::cout << key << ": " << val << std::endl;
}

在这里我们可以清晰的看出结构化绑定的语义。

1.1 更详细的阐述结构化绑定

结构化绑定中匿名变量非常重要,结构化绑定引入的新名字的都是指代的这个匿名遍历的成员、元素。

1.1.1 绑定到匿名对象

先考虑如下绑定到匿名变量:

代码语言:javascript
复制
auto [u, v] = ms;

可以看作:

代码语言:javascript
复制
auto e = ms; // e是匿名变量
aliasname u = e.i;
aliasname v = e.s;

这里需要注意一点的是,uv不是e.ie.s的引用。它们只是这两个成员的别名。其中e是匿名变量,所以我们不能直接访问e.ie.s,但是可以访问uv,例如:

代码语言:javascript
复制
std::cout << u << ": " << v << std::endl;

这个例子输出e.ie.s的值,它们是ms.ims.s的一份拷贝。

匿名变量e的生命周期同结构化绑定的存活时间一样,当结构化绑定离开作用域时,e会同时析构。

在这里我们还需要理解引用,通过结构化绑定,利用前面的例子:

代码语言:javascript
复制
MyStruct ms{42, "hello"};
auto [u, v] = ms;
ms.i = 100;
std::cout << u << ": " << ms.i << std::endl; // 42: 100
u = 101;
std::cout << u << ": " << ms.i << std::endl; // 101: 100

第一次输出42: 100,u是ms.i的一份拷贝,而修改了ms.i并不会影响u,因此输出是42: 100。第二次输出101:100,直接修改了u,同样u是ms.i的一份拷贝,修改了不会影响ms.i,因此输出是101: 100。现在我们换成引用:

代码语言:javascript
复制
auto& [u1, v1] = ms;
ms.i = 100;
std::cout << u1 << ": " << ms.i << std::endl;
u1 = 101;
std::cout << u1 << ": " << ms.i << std::endl;

此时第一次输出是100: 100 第二次输出是101: 101

同理,对返回值使用结构化绑定,上面规则一样成立。

代码语言:javascript
复制
auto [u, v] = getStruct();

转换为:

代码语言:javascript
复制
auto e = getStruct();
aliasname u = e.i;
aliasname v = e.s;

可以看到结构化绑定是绑定到一个新的对象,由返回值进行初始化,而不是直接绑定到返回值本身

另外,内存地址和对齐也是存在。如果成员有对齐,结构化绑定也会存在对齐,例如:

代码语言:javascript
复制
auto [u, v] = ms;
assert(&((MyStruct*)&u)->s == &v);

1.1.2 使用修饰符

结构化绑定中我们可以使用一些修饰符,如:const&

这里的修饰符是加在匿名变量上。

代码语言:javascript
复制
const auto& [u, v] = ms;

等价于

代码语言:javascript
复制
const auto& e = ms;
aliasname u = e.i;
aliasname v = e.s;

引用对结果的影响可以看前面绑定到匿名对象的例子。

1.1.3 修饰符并非修饰结构化绑定

修饰符修饰的是匿名变量,而不是结构化绑定。尽管在结构化绑定的时候会使用到auto,但是结构化绑定的类型不会退化(数组转指针、修饰符被忽略等)。例如:

代码语言:javascript
复制
struct S {
  const char x[6];
  const char y[3];
};

调用:

代码语言:javascript
复制
S s1;
auto [a, b] = s1;

我们查看a与b的类型发现,还是const char[6]与const char[3],说明并没有发生退化为指针,原因是修饰符并非修饰结构化绑定而是修饰初始化结构体绑定的对象, 这一点和使用auto初始化新对象很不一样,它会发生类型退化。

代码语言:javascript
复制
auto a2 = a;

退化为const char*

1.1.4 移动语义

结构化绑定也支持移动语义,例如:

代码语言:javascript
复制
MyStruct ms = {42, "Jim"};
auto&& [u, v] = std::move(ms);

等价于:

代码语言:javascript
复制
MyStruct ms = {42, "Jim"};
auto&& e = std::move(ms);
aliasname u = e.i;
aliasname v = e.s;

结构化绑定u和v指向匿名变量中的成员,该匿名变量是ms的右值引用。ms仍然持有它的值:

代码语言:javascript
复制
std::cout << ms.i << ": " << ms.s << std::endl;

此时还可以移动赋值u与v,例如v与ms.s关联。

代码语言:javascript
复制
std::string s = std::move(v);
std::cout << ms.s << std::endl;
std::cout << v << std::endl;
std::cout << s << std::endl;

此时ms.s与v输出的是未指定的值,s输出的是Jim。移动后的对象(如前面v)的状态是有效的,只是包含了未指定的值(unspecified value)。因此,输出它的值是没有问题的,但是不能断言输出的东西一定是什么。

这一点和直接移动ms的值给匿名变量稍有不同:

代码语言:javascript
复制

MyStruct ms1 = {42, "Jim"};
auto [u1, v1] = std::move(ms1);  // new entity with moved-from values from ms
std::cout << "ms1.s: " << ms1.s << std::endl;  // prints unspecified value
std::cout << "v1: " << v1 << std::endl;        // prints "Jim"

仍然可以移动n并赋值,或者用它赋予一个新的值,但是不会影响ms.s:

代码语言:javascript
复制
std::string s = std::move(v1);  // moves v1 to s
v1 = "Lara";
std::cout << "ms.s: " << ms.s << std::endl;  // prints unspecified value
std::cout << "v1: " << v1 << std::endl;      // prints "Lara"
std::cout << "s: " << s << std::endl;        // prints "Jim"

1.2 结构化绑定可以在哪使用

原则上,结构化绑定可以应用于public成员、C-style数组、类似tuple对象。具体如下:

  • public非静态成员 结构体或类中的非静态成员是public
  • 原生数组 绑定到每个元素
  • 任何类型,使用类似tuple的API

std::tuple_size<type>::value 返回元素数量std::tuple_element<idx,type>::type 返回第idx个元素的类型 一个全局或成员函数get<idx>()返回idx个元素的值

使用的时候需要元素或数据成员的数量必须匹配结构化绑定的名字的个数。不能跳过任何一个元素,也不能使用同一个名字多次,但是可以使用_跳过当前元素。

切记不要在同一命名空间使用多次_

代码语言:javascript
复制
auto [_, v1] = getStruct(); // OK
auto [_, v2] = getStruct(); // ERROR: name _ already used

嵌套对象分解是不支持的,诸如:

代码语言:javascript
复制
auto [a,(b,c)] = (3,(4,2));

1.2.1 结构体和类

结构化绑定不适用于继承,所有非静态数据成员必须在同一个类。

换句话说,这些数据成员要么全是基类,要么全是子类。

例如:

代码语言:javascript
复制
struct B {
  int a{1};
  int b{2};
};

struct D1 : public B {};
struct D2 : public B {
  int c{3};
};

struct B1 {};

struct D3 : public B1 {
  int c{3};
};

int main() {
  auto [x, y] = D1{};     // 全部来自于基类
  auto [z] = D3{};        // 全部来自于子类
  auto [i, j, k] = D2{};  // 编译时错误 无法分解
  return 0;
}

1.2.2 原生数组

数组长度已知的情况下,可以结构化绑定到多个变量上。

C++允许我们返回带长度的数组引用:

代码语言:javascript
复制
auto getArr() -> int(&)[2] {

}
auto [x, y] = getArr();

同样可以对std::array进行结构化绑定。

1.2.3 std::pair,std::tuple和std::array

结构化绑定可扩展,可以为任何类型添加结构化绑定,标准库中使用到的有std::pair,std::tuple,std::array

1.std::array

代码语言:javascript
复制
std::array<int, 4> getArray();
auto [i,j,k,l] = getArray();

同样,如果需要更改原生数组里面的值可以改为:

代码语言:javascript
复制
std::array<int, 4> stdarr{1, 2, 3, 4};
auto& [i, j, k, l] = stdarr;
i += 10;

2.std::tuple

代码语言:javascript
复制
std::tuple<char, float, std::string> getTuple() {
  std::tuple<char, float, std::string> tp('1', 6.6, "hello");
  return tp;
}
auto [a, b, c] = getTuple();

3.std::pair

处理关联/无序容器的insert()调用的返回值,使用结构化绑定使代码可读性更强,可以更加清晰的表达自己的一图,而不是依赖与std::pair的first与second。

C++ 17之前写法:

代码语言:javascript
复制
std::map<std::string, int> coll;
auto ret = coll.insert({"new",42});
if (!ret.second) {
  // if insert failed, handle error using iterator pos:
}

C++17之后:

代码语言:javascript
复制
std::map<std::string, int> coll;
auto [pos,ok] = coll.insert({"new",42});
if (!ok) {
  // if insert failed, handle error using iterator pos:
}

可以看到C++17提供了一种表达力更强的带初始化的if。

4.为pair和tuple的结构化绑定赋值

声明了结构化绑定之后,通常不能一次性修改全部结构化绑定,因为结构化绑定是一次性声明所有。如果重新赋值是std::pair<>std::tuple<>,可以使用std::tie();

代码语言:javascript
复制
std::tuple<char,float,std::string> getTuple() { }
auto [a, b, c] = getTuple();
std::tie(a, b, c) = getTuple(); // next returned tuple

尤其适用于在循环中不停地赋值例如下面查找例子:

在text中查找sub

代码语言:javascript
复制
std::string text =
    "Lorem ipsum dolor sit amet, consectetur adipiscing elit,"
    " sed do eiusmod tempor incididunt ut labore et dolore magna aliqua";

std::string sub = "dolor";
std::boyer_moore_searcher bm{sub.begin(), sub.end()};
for (auto [beg, end] = bm(text.begin(), text.end()); beg != text.end();
     std::tie(beg, end) = bm(end, text.end())) {
  // find sub in text
  std::cout << "find sub string in text string, start pos is "
            << beg - text.begin() << std::endl;
}

1.3 为结构化绑定提供类似tuple的API

只要我们的类型实现了类似tuple的API,那么就可以针对该类型使用结构化绑定,这样便可以从std::pair<>,std::tuple<>,和std::array<>推广到任意类型。

1.3.1 只读结构化绑定

首先,定义一个类:

代码语言:javascript
复制
class Customer {
 private:
  std::string first;
  std::string last;
  long val;

 public:
  Customer(std::string f, std::string l, long v)
      : first(std::move(f)), last(std::move(l)), val(v) {}
  std::string getFirst() const { return first; }
  std::string getLast() const { return last; }
  long getValue() const { return val; }
};

提供类似tuple的API:

代码语言:javascript
复制
template <>
struct std::tuple_size<Customer> {
  static constexpr int value = 3;
};
template <>
struct std::tuple_element<2, Customer> {
  using type = long;
};

template <std::size_t Idx>
struct std::tuple_element<Idx, Customer> {
  using type = std::string;
};
template <std::size_t>
auto get(const Customer& c);
template <>
auto get<0>(const Customer& c) {
  return c.getFirst();
}
template <>
auto get<1>(const Customer& c) {
  return c.getLast();
}
template <>
auto get<2>(const Customer& c) {
  return c.getValue();
}

对get进行封装,使用编译时if特性:

代码语言:javascript
复制
template <std::size_t I>
auto get(const Customer& c) {
  static_assert(I < 3);
  if constexpr (I == 0) {
    return c.getFirst();
  } else if constexpr (I == 1) {
    return c.getLast();
  } else {  // I == 2
    return c.getValue();
  }
}

有了这些API,结构化绑定就可以在main函数中使用。

代码语言:javascript
复制
int main() {
  Customer c("Tim", "Starr", 42);
  auto [f, l, v] = c;
  std::cout << "f/l/v: " << f << ' ' << l << ' ' << v << '\n';
  // modify structured bindings:
  std::string s = std::move(f);
  l = "Waters";
  v += 10;
  std::cout << "f/l/v: " << f << ' ' << l << ' ' << v << '\n';
  std::cout << "c: " << c.getFirst() << ' ' << c.getLast() << ' '
            << c.getValue() << '\n';
  std::cout << "s: " << s << '\n';
  return 0;
}

输出:

代码语言:javascript
复制
f/l/v: Tim Starr 42
f/l/v:  Waters 52
c: Tim Starr 42
s: Tim

1.3.2 结构化绑定写操作

在上述Customer中对成员函数进行修改:

代码语言:javascript
复制
class Customer {
 private:
  std::string first;
  std::string last;
  long val;

 public:
  Customer(std::string f, std::string l, long v)
      : first(std::move(f)), last(std::move(l)), val(v) {}
  const std::string& firstname() const { return first; }
  std::string& firstname() { return first; }
  const std::string& lastname() const { return last; }
  std::string& lastname() { return last; }
  long value() const { return val; }
  long& value() { return val; }
};

要支持读写,需要对常量引用和非常量引用准备getter重载:分别支持非常量对象、常量对象、可移动对象,为了返回引用,应该使用decltype(auto)

代码语言:javascript
复制
// define specific getters:
template <std::size_t I>
decltype(auto) get(Customer& c) {
  static_assert(I < 3);
  if constexpr (I == 0) {
    return c.firstname();
  } else if constexpr (I == 1) {
    return c.lastname();
  } else {  // I == 2
    return c.value();
  }
}
template <std::size_t I>
decltype(auto) get(const Customer& c) {
  static_assert(I < 3);
  if constexpr (I == 0) {
    return c.firstname();
  } else if constexpr (I == 1) {
    return c.lastname();
  } else {  // I == 2
    return c.value();
  }
}
template <std::size_t I>
decltype(auto) get(Customer&& c) {
  static_assert(I < 3);
  if constexpr (I == 0) {
    return std::move(c.firstname());
  } else if constexpr (I == 1) {
    return std::move(c.lastname());
  } else {  // I == 2
    return c.value();
  }
}

std::tuple_size<>std::tuple_element<>不变。

代码语言:javascript
复制
// provide a tuple-like API for class Customer for structured bindings:
template <>
struct std::tuple_size<Customer> {
  static constexpr int value = 3;  // we have 3 attributes
};
template <>
struct std::tuple_element<2, Customer> {
  using type = long;  // last attribute is a long
};
template <std::size_t Idx>
struct std::tuple_element<Idx, Customer> {
  using type = std::string;  // the other attributes are strings
};

同样,getter方法也可以不用编译时if,如下:

代码语言:javascript
复制
// 支持非const对象
template <std::size_t>
decltype(auto) get(Customer& c);
template <>
decltype(auto) get<0>(Customer& c) {
  return c.firstname();
}
template <>
decltype(auto) get<1>(Customer& c) {
  return c.lastname();
}
template <>
decltype(auto) get<2>(Customer& c) {
  return c.value();
}
// 支持const对象
template <std::size_t>
auto get(const Customer& c);
template <>
auto get<0>(const Customer& c) {
  return c.firstname();
}
template <>
auto get<1>(const Customer& c) {
  return c.lastname();
}
template <>
auto get<2>(const Customer& c) {
  return c.value();
}

// 支持移动对象
template <std::size_t>
decltype(auto) get(Customer&& c);
template <>
decltype(auto) get<0>(Customer&& c) {
  return std::move(c.firstname());
}
template <>
decltype(auto) get<1>(Customer&& c) {
  return std::move(c.lastname());
}
template <>
decltype(auto) get<2>(Customer&& c) {
  return std::move(c.value());
}

调用:

代码语言:javascript
复制
int main() {
  Customer c("Tim", "Starr", 42);
  auto [f, l, v] = c;
  std::cout << "f/l/v: " << f << ' ' << l << ' ' << v << '\n';
  // modify structured bindings via references:
  auto&& [f2, l2, v2] = c;
  std::string s = std::move(f2);
  f2 = "Ringo";
  v2 += 10;
  std::cout << "f2/l2/v2: " << f2 << ' ' << l2 << ' ' << v2 << '\n';
  std::cout << "c: " << c.firstname() << ' ' << c.lastname() << ' ' << c.value()
            << '\n';
  std::cout << "s: " << s << '\n';
  return 0;
}

输出:

代码语言:javascript
复制
f/l/v: Tim Starr 42
f2/l2/v2: Ringo Starr 52
c: Ringo Starr 52
s: Ti
学习自Cpp17TheCompleteGuide
本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2020-10-15,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 光城 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 1.结构化绑定
    • 1.1 更详细的阐述结构化绑定
      • 1.1.1 绑定到匿名对象
      • 1.1.2 使用修饰符
      • 1.1.3 修饰符并非修饰结构化绑定
      • 1.1.4 移动语义
    • 1.2 结构化绑定可以在哪使用
      • 1.2.1 结构体和类
      • 1.2.2 原生数组
      • 1.2.3 std::pair,std::tuple和std::array
    • 1.3 为结构化绑定提供类似tuple的API
      • 1.3.1 只读结构化绑定
      • 1.3.2 结构化绑定写操作
相关产品与服务
容器服务
腾讯云容器服务(Tencent Kubernetes Engine, TKE)基于原生 kubernetes 提供以容器为核心的、高度可扩展的高性能容器管理服务,覆盖 Serverless、边缘计算、分布式云等多种业务部署场景,业内首创单个集群兼容多种计算节点的容器资源管理模式。同时产品作为云原生 Finops 领先布道者,主导开源项目Crane,全面助力客户实现资源优化、成本控制。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档