前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >初始化|这些年踩过的坑

初始化|这些年踩过的坑

作者头像
高性能架构探索
发布2023-11-24 17:49:37
2110
发布2023-11-24 17:49:37
举报
文章被收录于专栏:技术随笔心得

你好,我是雨乐!

最近在整理Modern CPP的某些新特性,恰好到了这块,所以就聊聊咯~~

统一初始化又称为列表初始化,自C++11引入,使用花括号(Brace-initialization)方式,主要目的是为了简化和统一不同的初始化方式,提高代码的可读性和可维护性,同时减少了某些特殊情况下可能出现的二义性。是Modern C++开发人员最应该了解和掌握的新特性之一。它的出现,消除了以前在初始化基本类型、聚合类型和非聚合类型、以及数组和标准容器之间的区别,以提供更一致的初始化语法。

目的

在C++11之前,初始化对象的方式有多种,包括:

1.直接初始化:Type variable(value);2.拷贝初始化:Type variable = value;3.列表初始化:Type variable{value};Type variable = {value};4.默认初始化:Type variable;

这些初始化方式依赖于其具体类型

•对于基础类型,则可以使用赋值方式直接初始化

代码语言:javascript
复制
int a = 42;
double b = 1.2;

•对于类类型,在其只有一个参数的情况下,也可以使用赋值方式进行初始化

代码语言:javascript
复制
class foo
{
  int a_;
public:
  foo(int a):a_(a) {}
};
foo f1 = 42;

•对于非聚合类,也可以使用后面跟括号的方式(括号中传入参数),对于不需要参数的则不能添加括号,否则编译器会认为是函数声明

代码语言:javascript
复制
foo f1;           // default initialization
foo f2(42, 1.2);
foo f3(42);
foo f4();         // function declaration

•聚合类可以通过花括号的方式进行初始化

代码语言:javascript
复制
bar b = {42, 1.2};
int a[] = {1, 2, 3, 4, 5};

除了以上初始化方式之外,对于标准容器来说,都是先声明一个对象,然后通过插入的方式进行初始化,不过,std::vector是个例外,其可以从先前使用聚合初始化初始化的数组中分配,如下:

代码语言:javascript
复制
nt arr[] = {1, 2, 3, 4, 5}; // 使用聚合初始化初始化数组

std::vector<int> vec(std::begin(arr), std::end(arr)); // 使用数组的值初始化 std::vector

用法

在上节中,我们看到在C++11之前有多种初始化方式,开发人员往往需要对每种的场景都需要了解,以防止性能损失或者编译错误,正是为了解决这个问题,自C++11起,引入了统一初始化(List initialization或者Uniform initialization)。

统一初始化,用{}方式进行初始化,如下:

代码语言:javascript
复制
T object {other};   
T object = {other}; 

下面是关于统一初始化的一些例子:

•标准库中的容器

代码语言:javascript
复制
std::vector<int> v { 1, 2, 3 };
std::map<int, std::string> m { {1, "one"}, { 2, "two" }};

•动态数组分配

代码语言:javascript
复制
int* arr2 = new int[3]{ 1, 2, 3 };

•数组

代码语言:javascript
复制
int arr1[3] { 1, 2, 3 };

•内置类型

代码语言:javascript
复制
int i { 42 };
double d { 1.2 };

•自定义类型

代码语言:javascript
复制
class foo
{
  int a_;
  double b_;
public:
  foo():a_(0), b_(0) {}
  foo(int a, double b = 0.0):a_(a), b_(b) {}
};
foo f1{};
foo f2{ 42, 1.2 };
foo f3{ 42 };

•POD类型

代码语言:javascript
复制
struct bar { int a_; double b_;};
bar b{ 42, 1.2 };

一些细节

在前面的两节中,分别讲解了Modern C++之前的初始化方式以及统一初始化方式,从使用方式上来看,更加统一,显然统一初始化是我们进行初始化时候的首选,当然了,需要注意一些细节,尤其是对于存在参数为std::initializer_list的容器类型来说。

代码语言:javascript
复制
// a vector containing two elements: 10 and 20
std::vector<int> v{10, 20};

// a vector containing 10 elements: all 20
std::vector<int> w(10, 20);

emm!!上述代码的区别其实已经在注释里面讲了,对于v来说用的是列表初始化方式,其构建了一个vector,里面有2个元素10和20;对于w,其也是构建了一个vector,里面有10个元素,且每个元素的值都为20,下面是STL中这块的源码:

代码语言:javascript
复制
vector(size_type __n, const _Tp& __value,
          const _Allocator& __a = _Allocator())
       : _Base(__n, __value, __a), _M_guaranteed_capacity(__n) { }
       
vector(initializer_list<value_type> __l,
          const allocator_type& __a = allocator_type())
       : _Base(__l, __a), _Safe_base(),
     _M_guaranteed_capacity(__l.size()) { }
模板

继续看个例子:

代码语言:javascript
复制
template <class T>
T copy(T const& val) {
    return T{val};
}
        
auto a = copy(std::string{});
auto b = copy(std::vector<int>{});
auto c = copy(std::vector<std::any>{});

好了,请闭眼思考下,看看上面abc的内容分别是什么?

首先,创建了一个模板函数copy,其内部实现就是用返回一个参数的拷贝,需要注意的是使用的统一初始化的方式。

a是一个空字符串的拷贝,b是一个空std::vector的拷贝,那么c会不会像b一样,也是空std::vector<>的拷贝呢?确实,其类型为std::vector<std::any>,但是,size却不是0,而是1,这是因为std::any可以是任何类型变量的原因~

接着看另外一个例子:

代码语言:javascript
复制
template<typename T>
std::vector<T> create()
{
    return std::vector<T>{10};
}
 
int main()
{
    auto a = create<std::string>();
    auto b = create<int>();
    auto c = create<char>();
    auto d = create<std::vector<int>>(); 

    std::cout << a.size() << " " << b.size() << " " << c.size() << " " << d.size();
}

上述代码中,abcd的类型就不需要多说了吧,我们先猜测下上述代码的输出。。。

emm,编译运行后,输出结果为10 1 1 10,是不是很奇怪,下面进行简单的分析。

在模板函数create中,使用统一初始化并返回,对于a来说,因为其传入的是std::string,那么在函数create中,将变成**return std::vector<std::string>{10}**,乍一看,应该是用10进行初始化,但因为数据类型是std::string,所以用10进行初始化失败,那么退而求其次,调用了std::vector<std::string>(10);这就是a的size为10的原因,同理,b和c的size是1,d的size为10。

类型推导

再看一个例子:

代码语言:javascript
复制
std::vector<int> v{1, 2, 3, 4, 5};
std::vector<int> w{v.begin() + 1, v.end()};
std::vector w2{v.begin() + 1, v.end()};

上述几个例子中,都是通过统一初始化的方式进行初始化,v和w的类型一样,都是std::vector<int>,但是w2的类型却是std::vector<std::vector<int>::iterator>

还记得在前面的例子中,使用统一初始化的时候,相当于插入一个元素么,即:

代码语言:javascript
复制
std::vector<int> v1{1, 2, 3};
std::vector v2{std::vector{1, 2}};

在上述代码中v1的值有3个,分别为1 2 3,那么按照该规则,v2的类型岂不是std::vector<std::vector<int>>,在一开始学习这块的时候,我曾经也这么以为~~~通过cppinsights分析,发现v2的类型是std::vector<int>,如果想让v2的类型是vector的话,则必须显示指定类型,即如下:

代码语言:javascript
复制
std::vector<std::vector<int>> v2{std::vector{1, 2}};
类型转换

统一初始化的另外一个特点是防止缩小初始化,想必我们都写过如下这种代码:

代码语言:javascript
复制
double d = 1.5;
int x = d; // x is 1 (double converts to int).

如果使用统一初始化的话:

代码语言:javascript
复制
int x{d}; // ERROR: cannot be narrowed.

则编译器会报错,为了解决编译器报错的问题,可以采用如下方式:

代码语言:javascript
复制
int x{(int)d};              
int x{int(d)};              
int x{static_cast<int>(d)}; // modern C++建议的方式
解析

经常能够遇到下面这个问题,是编译器在某些情况下解决语法歧义的方式:

代码语言:javascript
复制
class MyClass {};
MyClass f();

在编译的时候,会报错如下:

代码语言:javascript
复制
remove parentheses to default-initialize a variable

意思是去掉后面的**()以便调用默认构造函数。之所以有这个报错,是因为当C++无法区分“对象创建”和“函数声明”时,编译器默认将该语句解释为“函数声明”。**

继续看如下代码:

代码语言:javascript
复制
std::vector<int> v(5, 0); // {0, 0, 0, 0, 0}.

这段代码很简单吧,就是初始化vector,但是如果将其放入如下代码中,则编译器会报错,虽然我们的目的是进行初始化:

代码语言:javascript
复制
class MyClass {
 public:
  MyClass() { ... }

 private:
  std::vector<int> v(5, 0); // ERROR
};

为了解决这种这个问题,可以采用如下方式:

代码语言:javascript
复制
class MyClass {
 public:
  MyClass() : v(5, 0) { ... }

 private:
  std::vector<int> v;
};

也可以这样:

代码语言:javascript
复制
class MyClass {
 public:
  MyClass() { ... }

 private:
  std::vector<int> v = std::vector<int>(5, 0);
};
初始化列表

在前面内容中,有提到过,统一初始化,又称为列表初始化,列表无非是以std::initializer_list这种方式存在。编译器有个特点,对于以花括号初始化的方式则认为是统一初始化,如果构造函数中同样存在std::initializer_list为参数的构造函数,那么则优先调用

代码语言:javascript
复制
class MyClass {
 public:
  MyClass(int x, double y) { ... }
  MyClass(std::initializer_list<bool> z) { ... }
};

int main() {
  MyClass obj{5, 1.0};
};

我们可能期望MyClass obj{5, 1.0};调用第一个构造函数(以int和double作为参数的构造函数),但由于存在以std::initializer_list参数作为参数的构造函数重载,因此该构造函数将是首选。在这种情况下,编译器甚至会抛出错误,因为它检测到从int和double的缩小转换bool。试想一下,如果不涉及缩小转换(例如,第二个构造函数接受 in std::initializer_list<double>,则代码将使用第二个构造函数(在初始值设定项列表中int 5转换为double 5.0)默默执行,而开发人员则认为它正在使用第一个构造函数,emm,后果不堪设想~~

在上面提了,编译器会优先调用参数为std::initializer_list的构造函数,但是有个例外:

代码语言:javascript
复制
class MyClass {
 public:
  MyClass() { ... }
  MyClass(std::initializer_list<int> z) { ... }
};

int main() {
  MyClass obj{}; // Calls the first constructor.
};

如果我们想让编译器调用第二个构造函数,可以像如下这样写:

代码语言:javascript
复制
MyClass obj( {} );
MyClass obj{ {} };

结语

这块终于写完了,一边写一边改,内容确实太杂了,本来想的是把遇到的坑都写出来,一时半会想不起来,只能等以后了。

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2023-11-23,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 高性能架构探索 微信公众号,前往查看

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

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

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 目的
  • 用法
  • 一些细节
    • 模板
      • 类型推导
        • 类型转换
          • 解析
            • 初始化列表
            • 结语
            相关产品与服务
            容器服务
            腾讯云容器服务(Tencent Kubernetes Engine, TKE)基于原生 kubernetes 提供以容器为核心的、高度可扩展的高性能容器管理服务,覆盖 Serverless、边缘计算、分布式云等多种业务部署场景,业内首创单个集群兼容多种计算节点的容器资源管理模式。同时产品作为云原生 Finops 领先布道者,主导开源项目Crane,全面助力客户实现资源优化、成本控制。
            领券
            问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档