矿坑系列 ── Structured binding declaration

🐳 矿坑系列 ── Structured binding declaration

tags: C++

此篇由我的 hackmd 搬運而來,下方這是我的 hackmd 目錄,有興趣的朋友也可以來逛逛。
点此回到 C++笔记 目录

这是矿坑系列的第一篇文章,希望能够每周都有一篇的产量,内容主要会是我详细去看某个 C++11 以后的语法的笔记。我平常主要是在 hackmd 上写笔记的,由于之前学习时受过了许多人的帮助,因此就想着也搬上 CSDN 来回馈一下大家,但还不太会操作,如果大家想看有CSS美美的版本可以到我的 hackmd 内查看,不过是繁体字就是了。

前言

Structured binding declaration 是C++17 加入的一个新特性,它让我们能够更简单地去处理多个回传值或多变数的情况,通常会在要接 tuple_like 的容器或Struct 的回传值时搭配auto来使用。

我尽量将原里理解并写在了「介绍及原理」的部分,有一大部分是翻译了官方的文件,原先只是想弄个翻译,结果翻着翻着发现官方有些地方讲的不是让人很明白,于是就又加上了自己的文字,渐渐地就变一篇文件了,如果只想知道如何使用的朋友可以直接跳到下方的应用部分。

另外有些地方我也不是百分之百的了解了,如果有大佬愿意帮忙补充亦或是纠正我的错误就太感谢了!

语法

主要有三种初始化的方式:

1. attr(opt) cv(opt) auto ref-qualifier(opt) [idendentifier-list] = expression;
2. attr(opt) cv(opt) auto ref-qualifier(opt) [idendentifier-list]{ expression };
3. attr(opt) cv(opt) auto ref-qualifier(opt) [idendentifier-list]( expression );

💡 opt 代表的是 optional,可加可不加的意思。

  • attr 🐳
    指的是 attrubutes,可加可不加。

  • cv 🐳
    可能是cv-qualifier,后方需加上auto,需要的话也可以加上static、thread_local 之类的储存类说明符,但不推荐使用到volatile。

  • ref-qualifier 🐳
    &&&,可加可不加,取决于你的需求。

  • identifier-list 🐳
    这里放妳要使用的变数名称,他实际上不是变数而是标示符,它们之间需要以逗点 , 隔开,后方会有例子。

  • expression 🐳
    表达式,通常会放array、tuple-like 容器或是个没有union 成员的Class,语法上会是assignment-expression,它们不能是 throw 表达式,并且在top-level 不能有逗号运算符,这里应该是指expression 能够有sub-expression,而它要的是最上层的那个(感谢marty大佬)。另外,expression 内的变数名不能和 identifier-list 内的变数名相同,简单来说就是不能重复宣告同样名字的变数。

介绍及原理

Structed binding 会在你现在的 scope 内采用你identifier-list 里给的标示符,并且将其连结到你expression 里写的元素或子物件。采用时它会先创造出一个特殊的变数来存取你的初始化叙述(initializer),型态取决于你的expression,这个变数的名称这里我们先取作__e ,由于__e 可能是个容器或参考,所以我们给他取叫 initializer,没看过这个词的朋友不用太担心,而 __e 在存取时有一些规则:

  • 如果expression 是个A型态的array,而且你没有使用ref-qualifier,那么__e 会是原先expression 计算结果的复本,型态会是cv A,cv指的就是cv-qualifier 。而 __e 内的元素会依据你使用的初始化方式(最上方写的三种方式)来初始化。

    如果你使用的是第一种(=号)方式,那么__e 内的元素会使用复制初始化来初始化为你expression 内相对应的元素;

    若你使用的是第二或第三种方法,那么__e 内的元素会使用直接初始化来初始化为你expression内相对应的元素。

  • 如果不是上面的那种情况,那么编译器先将 Structured Binding 改写,直接使用 __e 这个名称作为原先 expression 的复本,像这样:

    1. attr(opt) cv(opt) auto ref-qualifier(opt) __e = expression;
    2. attr(opt) cv(opt) auto ref-qualifier(opt) __e{ expression };
    3. attr(opt) cv(opt) auto ref-qualifier(opt) __e( expression );

    __e 会是个匿名的tuple-like 的容器(没ref-qualifier时) 或是tuple-like 的容器的参考(有ref-qualifier时),简短来說妳有写& 这个__e 就会是个参考,如果没写就是个容器。接着编译器会去看它是否符合Tuple-Like" Binding Protoco ( tuple-like 连结协定),简单来说会长这样:

    • std::tuple_size<__E>::value 必须是个格式正确的整数常量表达式 (integer constant expression)。
    • identifier-list 内元素的数量必须与 std::tuple_size<__E>::value 相同。
    • 如果上面两项有其中一项不符合,便去检查这个 Class 的成员变数是否都为 public,如果不是(有 private 的成员变数),则编译错误

    接着identifier-list 内的元素便会「连结」到__e 内相对应的元素,这也是妳有写& 时,对identifier-list 内的元素做改动就能改动到原容器的原因,因为__e 是个参考,举个例子:

    std::tuple<int,int> a{1,2}
    auto [x,y] = a; // __e 是个容器, x 与 std::get<0>(__e) 连结, y 与 std::get<1>(__e) 连结
    auto &[x,y] = a; // __e 是 a 的左值参考, x 与 std::get<0>(__e) 连结, y 与 std::get<1>(__e) 连结
    auto &&[x, y] = std::make_tuple( 1, 2 ); // __e 是右边那个tuple 的右值参考,x 与std::get<0>(__e) 连结, y 与std::get <1>(__e) 连结
    

    Code 有点长,大家可以复制下来看,在网页上可能不太好阅读。可以看见内部是使用std::get<>() 来存取元素的,因此妳的expression 必须是个回传tuple-like 容器的叙述,否则妳的__e 不会是个tuple-like 的容器(或容器的参考),那个也就无法使用std::get<>() 了。

    💡 这边只举了tuple-like 容器的例子,因为原生阵列没有复制建构子,也就是说他不能被改写成上面那三种样式,也就不能用那三种方法初始化,可以看下面这个例子,它会喷错:

    int a[2]{ 1, 2 };
    int b[2] = a;
    

    所以会需要另外规定方式来初始化。

    「连结」这个动作无法以C++ 语言描述,妳可以把他想像成参考,又或是宏定义,但要记得他不是,他是C++ 语言的本身,没办法用C++ 写出来,已经类似语言特性的概念了,就好像我们无法自己实作function-body 的大括号一样(感谢Cy解释)。

    💡 官方文件是这么写的: Structured Binding 像是个参考,它是某个已经存在的物件的别名,但 Structured Binding 不是参考,它不需要是个引用类型。

    挺玄学的,我自己是用「类似宏定义」来理解的,底下也会如此解释,但各位要记得它不是宏定义,也许是为了确保将标示符丢进 std::remove_reference_t <decltype((标示符))>() 时型态要与连结到的元素丢进 std::remove_reference_t<decltype((连结到的元素))>() 一样才如此设计的。

接下来我会详细的讲解一下内部的原理,这里用__E 来表示__e 的型态,也就是说__E 为初始化叙述(initializer) 的型态,另外我们也可以说__Estd::remove_reference_t<decltype((__e))> 等价。

上述的初始化结束后,它会根据 __E 的状况来进行连结,会有三种情况:

  • 如果 __E 是个 array 型态 ,那么 identifier-list 内的元素会与初始化叙述(initializer) 内相对应的元素连结

    这种情况下,每个identifier-list 内的标示符会是一个左值(lvalue),与初始化叙述(initializer) 内相应的元素连结,也因此,identifier-list 内的标示符数量需要与array 内的变数数量一样多,看一下下面这个例子:

    int a[2] = {1,2};
    auto [x,y] = a;
    

    auto [x,y] = a; 会创建一个名字叫__e 的array __e[2],利用[复制初始化](https://en.cppreference.com/w/cpp/language/ copy_initialization)来初始化__e[2],之后x 与y 分别会与__e[0]__e[1] 连结,你可以把他们想像成参考,或是宏定义,但要记住它们实际上不是。

    如果有写 ref-qualifier 且 expression 回传的是 lvalue,则 identifier-list 内的元素会间接与 a 内的元素连结,对 identifier-list 内的元素的操作将会反应到 a 的元素上:

    int a[2] = {1,2};
    auto& [x,y] = a;
    

    auto& [x,y] = a; 会创建一个名字叫 __e 的参考引用expression 的计算结果,而identifier-list 内的元素则会透过 __e 间接变为a 内元素的参考,可以把他想像成这样:

    int a[2] = {1,2};
    auto & __e = a; // 等价于 int(&e)[2] = a;
    #define x __e[0]
    #define y __e[1]
    

    而如果 expression 回传的是 rvalue,则 identifier-list 内的元素会与 __e 内的元素连结:

    auto && [ x, y, z ] = ( int[] ){ 1, 2, 3 };
    

    可以把他想像成这样:

    auto &&__e = ( int[] ){ 1, 2, 3 };
    #define x __e[0]
    #define y __e[1]
    #define z __e[2]
    

    当然上面这两个例子都是伪代码,内部当然不是这样的,连结无法以 C++ 语言来描述,x 与 y 仅仅是标示符,所以不会是上面这个样子,这只是个示意。

  • 如果__E 是个没有union 成员的Class 型态,而且 std::tuple_size<__E> 是个有成员的完全型(不用管这个成员的型态或可访问性如何),简单来说就是__e 能够做成tuple-like 的容器,符合 tuple-like 连结协定,那么就会使用 tuple-like 连结协定来进行连结

    与前面提到的一样,首先std::tuple_size<__E>::value 必须是个格式正确的整数常量表达式(integer constant expression),并且identifier-list 内元素的数量必须与std::tuple_size <__E>::value 相同。

    再来对于每个标示符,都会连结一个元素(也就是__e 内的元素),元素的型态会类似是「std::tuple_element<i,__E>::type 的"引用"」 ,注意它是「引用」,i 指的是__e 内第i 个元素,如果这个型态对应的初始化叙述(initializer) 是左值,那这个变数就会是左值引用,如果是右值那就是右值引用。

    连结到的第 i 个元素详细如下:

    • 如果通过Class成员访问的方式在__E 的范围内查找到至少一个函式模板,且这个函式模板的第一个模板参数是个non-type参数,那么第i 个元素的初始化叙述(initializer) 会是e.get<i>()

    • 如果没有找到符合情况的函式模板,那么会使用argument-dependent lookup 的方式来呼叫get<i>(__e ),因此第i 个元素的初始化叙述(initializer) 会是e.get<i>(__e)

    在这些初始化叙述中,如果__e 是一个左值参考(这只会发生在你的ref-qualifier& ,或是你的初始化叙述是个左值而且ref-qualifier&& ,简单来说就是收合为& 时),那么你将expression 内相对应的元素会是一个左值(这听起来很废话,但重点在下一句)。

    否则expression 内相对应的元素会是一个消亡值(xvalue),因为内部实际上执行了一次完美转发(perfect-forwarding), 而i 会是个型态为std::size_t 的纯右值(prvalue),因此<i> 会被转换(解释)为模板参数列表

    💡 有三点提醒大家一下

    • identifier-list 内的标示符、__e 内的元素与 expression 内相对应的元素,这三个会有一样的生命周期。
    • 我们通常会直接称 identifier-list 内的标示符为「变数」,尽管它不是,但它使用上与变数基本上一样,概念也类似。
    • identifier-list 内第i个元素型态会是 std::tuple_element<i,E>::type

    看一下这个例子:

    float x{};
    char y{};
    int z{};
    
    std::tuple<float&,char&&,int> tpl(x,std::move(y),z);
    const auto& [a,b,c] = tpl;
    

    a 的名字叫做「Structured Binding」,连结到 tpl内第一个元素, decltype(a)float&
    b 的名字叫做「Structured Binding」,连结到 tpl内第二个元素, decltype(b)char&&
    c 的名字叫做「Structured Binding」,连结到 tpl内第三个元素, decltype(c)const int

  • 如果不是以上两种情况,则expression 内的每个non-static 成员变数都需要是个直接成员或是expression 的相同基类,而且Structured Binding 格式需要正确,让我们能够间接使用__e.name 来呼叫变数。你的 expression 内不能有匿名或是 union 的成员,identifier-list 内的标示符数量需要与 non-static 的成员变数数量相同。

    每个 identifier-list 内的标示符都会连结到相对应的成员变数,实际上是连结到 __e.m_im_i 表示第 i 个成员变数,另外 Structured Binding 支援 bit field 用法,

    看这个例子:

    #include <iostream>
    
    struct S {
        mutable int x1 : 2;
        volatile double y1;
        int z1;
    };
    
    int main() {
        using f = S;
        const auto [x, y, z] = f();
    
        return 0;
    }
    

    x 会是个整数左值标示符,连结到一个 2-bit 的整数元素 x1,y 会连结到 const volatile double 的元素 y1。

使用 Structured Binding

💡 记得要切换成 C++17 才能够使用。

现在我们举个简单的例子(来源),现在我要定义一个「人」的函式,人会有年龄、名字等等,因此它的回传型态会是一个 std::tuple<std::string, int>

std::tuple<std::string, int> CreatePerson() {
    return { "Cherno", 24 };
}

而在 main 内我们需要用到资料时,过去需要像这样:

std::tuple<std::string, int> person = CreatePerson(); // 当然你可以用auto
std::string &name = std::get<0>( person );
int &age = std::get<1>( person );

而对 tuple 熟悉的朋友可能会使用 std::tie

std::string name;
int age;
std::tie( name, age ) = CreatePerson();

好多了,我们不需要为了赋值多个变数而额外创个person,但它仍然需要3 行,又或许我们可以使用Struct,但在C++17 后多了一个新特性Structured Binding,现在我们只需要这样:

auto [name, age] = CreatePerson();

而它也不只限定 tuple-like 的容器,也可以与 Struct 和原生阵列连结,看一下这个例子(来源):

struct TeaShopOwner {
    int id;
    std::string name;
};

auto GetTeaShopOwner() -> TeaShopOwner {
    TeaShopOwner owner { 1, "test"};
    return owner;
}

auto [id, name] = GetTeaShopOwner(); // id = 1 , name = "test"

如此一来 id 便会等于 1,name 则会是 “test” 了。 Structured Binding 的另一个好处是可以搭配 Ranged-based for Loop

如此一来 id 便会等于 1,name 则会是 “test” 了。 Structured Binding 的另一个好处是可以搭配 Ranged-based for Loop 使用:

struct TeaShopOwner {
    int id;
    std::string name;
};

std::vector<TeaShopOwner> owners {{1, "COCO"}, {2, "1Shit"}};

// C++17 前的用法
for (const auto& owner : owners) {
    printf("Owner id = %d\n", owner.id);
}

// 使用 Structured Binding
for (const auto& [id, _] : owners) {
    printf("Owner id = %d\n", id);
}

留意上面的 Ranged-based for-loop 接住 owners 时,将第二个变数名称取为 _,我们通常会利用这个手法来表示 name 在回圈里不受「重视」。

以往 C++ 函数的回传值多是单一型别,如 bool, int。有了 Structured Binding 再搭配其他技巧,在处理回传值时更有弹性。

补充

  • 对成员的 get 进行查找时会忽略可访问性与非类型模板参数的确切类型。像是 template<char*> void get(); ,成员将导致使用成员解释,即使格式是错的。

  • 有些 [ 前方的声明仅适用于隐藏变数 __e ,而不适用 identifier-list 内的元素:

#include <cassert>
#include <tuple>

int main() {
   int a = 1, b = 2;
   const auto &[x, y] = std::tie( a, b ); // x and y are of type int&
   auto [z, w] = std::tie( a, b ); // z and w are still of type int&
   assert( &z == &a ); // passes
   
   return 0;
}
  • tuple-like 的意思是使用 std::tuple_size<> 会是个完全型,即使这个可能导致格式错误:
#include <iostream>
#include <tuple>

struct A {
	int x;
};

namespace std {
template <>
struct tuple_size<::A> {};
} // namespace std

int main() {
	auto [x] = A{}; // error; the "data member" interpretation is not considered.

	return 0;
}

参考资料

1. Structured binding declaration (since C++17) (文章部份来源、例子来源)

2. C 17嚐鮮:結構化繫結宣告(Structured Binding Declaration)

3. C++ 17 結構化綁定

4. C++ 学习指南基础(三)

5. C++17尝鲜:结构化绑定声明(Structured Binding Declaration)

6. C++17新特性(1) – 结构化绑定初始化(Structured binding declaration)

7. When does an Incomplete Type error occur in C++

8. std::remove_reference

9. remove_reference 引用移除工作原理

10. std::is_same

11. Storage class specifiers

12. STRUCTURED BINDINGS in C++ (例子来源)

13. if-with-initializer in structured binding declaration example ill formed?

14. [C++] - 中的复制初始化(copy initialization)

15. Attribute specifier sequence(since C++11)

16. cv (const and volatile) type qualifiers

17. Copy initialization

18. Is there a difference between copy initialization and direct initialization?

19. Initializers

20. What is a sub-expression in C?

21. std::tuple_size<std::tuple>

22. P0144R0

23. Structured bindings implementation underground and std::tuple

24. Member access operators

25. Template non-type parameters

26. Argument-dependent lookup

27. Template parameters and template arguments

28. std::get(std::tuple)

29. Structured binding declarations [dcl.struct.bind]

30. Understand structured binding in C++17 by analogy

31. Structured binding on const

32. Is always the address of a reference equal to the address of origin?

33. C++ primary expressions - Is it primary expression or not?

34. DAY 16:Structured Bindings (例子来源)

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值