IE盒子

帖子
查看: 100|回复: 1

C++的{}到底是什么,有多少种用处?

[复制链接]

2

主题

5

帖子

9

积分

新手上路

Rank: 1

积分
9
发表于 2023-2-7 14:43:08 | 显示全部楼层 |阅读模式
前言

我们之前已经就列表初始化那块内容详细的说明了{}并不代表什么列表初始化,或某一种初始化
但是,它所涉及的内容远不止列表初始化,或初始化的内容,还包含着重载决议,等等别的情况,我们这里,就{}所有的使用情况,详细介绍。
{}该叫什么?它是什么?

首先,第一个问题{}该叫什么? 我们应该称它为花括号初始化器列表
很多人或者说应该是书,喜欢直接将它称为初始化列表,首先,这肯定和列表初始化没什么关联,并不是反过来。我本人两种说法都不太喜欢,或许直接说花括号最好。因为名字里带初始化三个字,容易让人觉得:这就是某种初始化,或者和初始化强关联的东西;实际上,远不止如此。
花括号初始化器列表不是表达式因此它没有类型即decltype({1,2})非良构,没有类型意味着模板类型推导无法推导出与花括号初始化器列表相匹配的类型,因此给定声明template<class T>voidf(T);则表达式f({1, 2, 3})非良构。然而,模板形参可以另行推导,举个例子
template<typename S>
struct Test {
        Test(S a ,S b)noexcept {
                std::cout << a << ' ' << b << '\n';
        }
};

int main() {
        Test t{ 1,{} };
}
S的类型根据第一个实参推导,但也被用于第二个形参,我们传一个{}也没有任何问题,更加经典的可以看标准库的,如std::vector<int> V(std::istream_iterator<int>(std::cin), {}); ,或者下面这个
int main() {
        std::set<std::string>sets;
        std::copy(std::istream_iterator<std::string>{std::cin}, {},
                std::inserter(sets, sets.end()));
}
与std::initializer_list的关联

我知道,很多人就是觉得花括号初始化器列表就是std::initializer_list,虽然是经典错误,但是这也是有原因的。
对于使用关键词auto的类型推导中有一个例外,在复制列表初始化中将任何花括号初始化器列表均推导为std::initializer_list
auto p = { 1,2,3,4,5,6 };//复制列表初始化将花括号初始化器列表推导为std::initializer_list
auto p2  { 1,2,3,4,5,6 };//无法推导
与列表初始化的关联

语法

直接列表初始化
T 对象 { 实参1, 实参2, ... };(1)
T { 实参1, 实参2, ... }(2)
new T { 实参1, 实参2, ... }(3)
类 { T 成员 { 实参1, 实参2, ... }; };(4)
类::类() : 成员 { 实参1, 实参2, ... } {...(5)
复制列表初始化
T 对象 = { 实参1, 实参2, ... };(6)
函数 ({ 实参1, 实参2, ... })(7)
return { 实参1, 实参2, ... };(8)
对象 [{ 实参1, 实参2, ... }](9)
对象 = { 实参1, 实参2, ... }(10)
U ({ 实参1, 实参2, ... })(11)
类 { T 成员 = { 实参1, 实参2, ... }; };(12)
实际上关联无非就是这些语法上使用到了{}而已,我们举几个显著的例子
demo1

struct X{
    explicit  X(int a, int b) :a(a), b(b) { std::cout << "X(int a,int b)\n"; }

    int a{};
    int b{};
};

int main() {
    X x{ 1,2 };
    X x2( 1,2 );
    X x3 = { 1,2 };
}
给出以上代码,是否正确?
答案:错误
解释: 复制列表初始化(考虑 explicit 和非 explicit 构造函数,但只能调用非 explicit 构造函数)
demo2

struct X {
    explicit  X(int a, int b) :a(a), b(b) { std::cout << "X(int a,int b)\n"; }

    int a{};
    int b{};
};

X f() {
    return { 1,2 };
}

int main() {
    X x{ 1,2 };
    X x2(1, 2);
    auto ret = f();
}
给出以上代码,是否正确?
答案:错误
解释:return{1,2}是复制列表初始化,复制列表初始化(考虑 explicit 和非 explicit 构造函数,但只能调用非 explicit 构造函数)
与聚合初始化的关联

从初始化器列表初始化聚合体。是列表初始化的一种形式
语法

T 对象 = { 实参1, 实参2, ... };(1)
T 对象 { 实参1, 实参2, ... };(2)(C++11 起)
T 对象 = { .指派符1 = 实参1 , .指派符2 { 实参2 } ... };(3)(C++20 起)
T 对象 { .指派符1 = 实参1 , .指派符2 { 实参2 } ... };(4)(C++20 起)
1,2) 用通常的初始化器列表初始化聚合体。
3,4) 用指派初始化器初始化聚合体。
必须满足是聚合体,才能进行聚合初始化,我们这里不再强调什么是聚合体,看demo
demo1

std::array肯定是聚合体的,聚合初始化
template<class Ty,size_t size>
struct array {
    Ty* begin() { return arr; };
    Ty* end() { return arr + size; };
    Ty arr[size];
};
int main() {
    ::array arr{1, 2, 3, 4, 5};
    for (const auto& i : arr) {
        std::cout << i << ' ';
    }
}
std::array也是类似上面这种结构,只是如果要想不写明模板参数,还是得增加一个推导指引,才能合法
template<typename Tp, typename... Up>
array(Tp, Up...)->array<std::enable_if_t<(std::is_same_v<Tp, Up>&& ...), Tp>, 1 + sizeof...(Up)>;
demo2

union u { int a; const char* b; };

u a = {1};                   // OK:显式初始化成员 a
u b = {0, "asdf"};           // 错误:显式初始化两个成员
u c = {"asdf"};              // 错误:不能以 "asdf" 初始化 int

// C++20 指派初始化器列表
u d = {.b = "asdf"};         // OK:可以显示初始化非开头元素
u e = {.a = 1, .b = "asdf"}; // 错误:显式初始化两个成员
demo3

int main() {
        int n[]{ 1,2,3,4 };
        int n2[] = { 1,2,3,4 };
}
数组显然是聚合体,聚合初始化,没问题。
demo4

int main() {
        int* p = new int[] {1, 2, 3, 4, 5};
}
如果 初始化器 是带括号的实参列表,那么数组会被聚合初始化。(C++20 起

与直接初始化的关联

T对象{实参};
以花括号环绕的单个初始化器初始化一个非类类型对象(注意:对于类类型和其他使用花括号初始化器列表的初始化,见列表初始化)
int n{10};
与值初始化的关联

T对象{};(4)(C++11 起)
T{}(5)(C++11 起)
newT{}(6)(C++11 起)
::(...):成员{}{...}(7)(C++11 起)

与重载决议的关联

因为花括号初始化器列表没有类型,所以在将它用作重载函数调用的实参时,适用重载决议的特殊规则。
demo1

void f(const int(&)[]) { puts("const int(&)[]"); }
void f(const int(&)[2]) { puts("const int(&)[2]"); }

int main() {
    f({ 1,2,3 });
    f({ 1,2 });
}
请问打印多少?
答案:
const int(&)[] const int(&)[2]
解释:

  • 如果形参类型是“ N 个 T 的数组”(这只对到数组的引用发生),那么初始化器列表必须有 N 个或更少的元素,且所用的隐式转换序列是将列表(或空花括号对,如果 {} 小于 N)的每个元素转换到 T 所需的最坏隐式转换序列。
  • 如果形参类型是“ T 的未知边界数组”(这只对到数组的引用发生),那么所用的隐式转换序列是将列表的每个元素转换到 T 所需的最坏隐式转换序列。
demo2

void f(const int(&)[]) { puts("const int(&)[]"); }
void f(const int(&)[2]) { puts("const int(&)[2]"); }
void f(int(&&)[]) { puts("const int(&&)[]"); }

int main() {
    f({ 1,2,3 });
    f({ 1,2 });
}
请问打印多少?
答案:
const int(&&)[] const int(&&)[]
解释:当前语境右值引用优于const T&,不需要进行值类别的隐式转换,具体参见重载决议列表初始化中的隐式转换序列
demo3

struct X { int x, y; };

struct Y {
    Y(std::initializer_list<int>){}
};

void f(const int(&)[]) { puts("const int(&)[]"); }
void f(const int(&)[2]) { puts("const int(&)[2]"); }
void f(int(&&)[]) { puts("int(&&)[]"); }
void f(X) { puts("X"); }
void f(Y) { puts("Y"); }


int main() {
    f({ 1,2,3 });
    f({ 1,2 });
    f({ .x=1,.y=2 });
}
请问打印多少?
答案:
int(&&)[] int(&&)[] X
解释:参见重载决议列表初始化中的隐式转换序列
demo4

void f(int) { puts("int"); }

int main() {
        f(1.2);
        //f({1.2});//不符合窄化转换
        f({ 1 });
}
与内建的赋值运算符的关联

struct A {
        int x, y;
};

int main() {
        std::array<A, 2> arr{ 1,2,2,2 };
       
        arr[1] = { 3,3 };//重点
        for (const auto& [a, b] : arr) {
                std::cout << a << ' ' << b << '\n';
        }
        A x{ 1,2 };
        x = { 1,2 };//重点
}
就没有人好奇这里的内建的=用{}是为什么可以吗?还是大家一直以为有定义的operator=?
内建的直接赋值

直接赋值表达式的形式为
左操作数 = 右操作数(1)
左操作数 = {}(2)(C++11 起)
左操作数 = { 右操作数 }(3)(C++11 起)
对于内建运算符,左操作数 可以拥有任何非 const 标量类型,而 右操作数 必须能隐式转换到 左操作数 的类型。
直接赋值运算符期待以一个可修改左值作为它的左操作数,以一个右值表达式或花括号初始化器列表 (C++11 起)作为它的右操作数,并返回一个标识修改后的左操作数的左值。如果左操作数是位域,那么返回结果也是位域。
对于非类类型,首先将右操作数隐式转换到左操作数的无 cv 限定的类型,然后复制它的值到左操作数所标识的对象中。
当左操作数拥有引用类型时,赋值运算符修改被引用的对象。
如果左右操作数标识的对象之间有重叠,那么行为未定义(除非二者严格重叠且类型相同)。
当右运算数是花括号初始化器列表 (brace-init-list)时如果表达式 E1 拥有标量类型,那么表达式 E1 = {} 与 E1 = T{} 等价,其中 T 是 E1 的类型。表达式 E1 = {E2} 与 E1 = T{E2} 等价,其中 T 是 E1 的类型。如果表达式 E1 拥有类类型,那么语法 E1 = {args...} 会生成以花括号初始化器列表为实参对赋值运算符的一次调用,然后遵循重载决议规则选取适合的赋值运算符。需要注意的是,如果以某个非类类型为实参的非模板赋值运算符可用,那么它胜过 E1 = {} 中的复制/移动赋值,这是因为从 {} 到非类类型属于恒等转换,它优先于从 {} 到类类型的用户定义转换。(C++11 起)


  • data-draft-node="block" data-draft-type="table" data-size="normal" data-row-style="normal">
当右运算数是花括号初始化器列表 (brace-init-list)时如果表达式 E1 拥有标量类型,那么表达式 E1 = {} 与 E1 = T{} 等价,其中 T 是 E1 的类型。表达式 E1 = {E2} 与 E1 = T{E2} 等价,其中 T 是 E1 的类型。如果表达式 E1 拥有类类型,那么语法 E1 = {args...} 会生成以花括号初始化器列表为实参对赋值运算符的一次调用,然后遵循重载决议规则选取适合的赋值运算符。需要注意的是,如果以某个非类类型为实参的非模板赋值运算符可用,那么它胜过 E1 = {} 中的复制/移动赋值,这是因为从 {} 到非类类型属于恒等转换,它优先于从 {} 到类类型的用户定义转换。(C++11 起)


  • 重载决议恒等转换data-draft-node="block" data-draft-type="table" data-size="normal" data-row-style="normal">
当右运算数是花括号初始化器列表 (brace-init-list)时如果表达式 E1 拥有标量类型,那么表达式 E1 = {} 与 E1 = T{} 等价,其中 T 是 E1 的类型。表达式 E1 = {E2} 与 E1 = T{E2} 等价,其中 T 是 E1 的类型。如果表达式 E1 拥有类类型,那么语法 E1 = {args...} 会生成以花括号初始化器列表为实参对赋值运算符的一次调用,然后遵循重载决议规则选取适合的赋值运算符。需要注意的是,如果以某个非类类型为实参的非模板赋值运算符可用,那么它胜过 E1 = {} 中的复制/移动赋值,这是因为从 {} 到非类类型属于恒等转换,它优先于从 {} 到类类型的用户定义转换。(C++11 起)

当右运算数是花括号初始化器列表 (brace-init-list)时如果表达式 E1 拥有标量类型,那么表达式 E1 = {} 与 E1 = T{} 等价,其中 T 是 E1 的类型。表达式 E1 = {E2} 与 E1 = T{E2} 等价,其中 T 是 E1 的类型。如果表达式 E1 拥有类类型,那么语法 E1 = {args...} 会生成以花括号初始化器列表为实参对赋值运算符的一次调用,然后遵循重载决议规则选取适合的赋值运算符。需要注意的是,如果以某个非类类型为实参的非模板赋值运算符可用,那么它胜过 E1 = {} 中的复制/移动赋值,这是因为从 {} 到非类类型属于恒等转换,它优先于从 {} 到类类型的用户定义转换。(C++11 起)
以 volatile 限定的非类类型左值为内建直接赋值运算符的左操作数被弃用,除非该赋值表达式在不求值语境或是弃值表达式中出现。(C++20 起)
针对用户定义运算符的重载决议中,对于每个类型 T,下列函数签名参与重载决议:
T*& operator=(T*&, T*);
T*volatile & operator=(T*volatile &, T*);
对于每个枚举或成员指针类型 T(可有 volatile 限定),下列函数签名参与重载决议:
T& operator=(T&, T );
对于每对 A1 和 A2,其中 A1 是算术类型(可有 volatile 限定)而 A2 是提升后的算术类型,下列函数签名参与重载决议:

A1& operator=(A1&, A2);
总结

实际上我省略了很多,如重载决议,聚合体,包括很多概念,原因很简单,我们的侧重点只是展示{}能使用的场景有多么的广泛而已。注意看我们提到的初始化那块的语法,都是可以使用{}的,全部写demo的话就太长了,就稍微写了几个
其他的多数的一些标准库容器,使用{}不少是因为std::initializer_list,但并不全是,我相信如果你看完了前面的,你应该会深以为然
回复

举报

3

主题

8

帖子

14

积分

新手上路

Rank: 1

积分
14
发表于 2023-2-7 14:43:18 | 显示全部楼层
个人习惯还是只用来表示聚合的场景(包括不限于聚合类, 数组, initializer_list
回复

举报

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

快速回复 返回顶部 返回列表