Modern C++ 变量声明与定义总结:auto,{},initializer_list与构造函数重载

自C++ 11/14 标准推出后,C++变量声明与定义的方式也发生了一些改变,增加了一些更为现代化,并且不会引起歧义的设施。其主要是基于两点特性,第一是auto关键字,第二是基于{ }(大括号)来声明初始化器。Modern C++也鼓励使用这些新的特性来进行变量的声明与定义,这会使代码的清晰性,可读性,正确性,可维护性上都会得到改善。

不过这些新的特性虽然都很好用,但有必要对Modern C++的变量声明方式,作一次全面的总结。


参考资料总结:

Effective Modern C++:

条款01:理解模板参数的类型推导。

条款02:理解auto的类型推导。

条款05:请使用auto来进行显式的变量声明。

条款06:当auto推导的类型并非所愿时,使用显式的typed initializer idiom。(即使用static_cast)

条款07:定义变量时,注意区分()和{ }。


《Effective Modern C++》的作者是大名鼎鼎的Meyers,这本书也是《Effective C++》的续集,其在里面对C++11/14的新特性进行了详细的讨论,并且提出了关于现代C++的一系列实践原则。不过这本书暂时没有中文版,英文好的同学可以去买影印版,这本书和《Effective C++》一样值得认真学习。


Gotw (Guru of the week):(https://herbsutter.com/gotw/)

#1: Variable Initialization – or Is It?

#92: Auto Variables, Part 1.

#93: Auto Variables, Part 2.

#94: AAA Style (Almost Always Auto).


Gotw的作者是Herb Sutter,也是久负盛名的《Exceptional C++》系列书籍的作者,现任C++标准委员会的主席。

以上#1是对原来的条款的C++11/14版本的更新,特别补充了使用{ }的方式来进行变量的声明的方式。#92到#94是基于新特性的条款,讨论auto的作用效果以及使用实践。


上述两位大师的资料能够互为补充,互为印证,非常全地地解析了这两种特性,也给出了很多实践的指导规则。

其实看完上面的资料其实就没必要看我下面的总结了,我的总结暂时还很片面,可能还存在一些错误,主要是为了自我学习。


第零部分:初始化的方式分类

关于初始化的分类问题,《C++ Primer》并没有进行很好的总结。我在不同的材料上也看到过不同的总结方式,包括在Gotw,《Effective C++》系列书籍,《Exceptional C++》等书籍关于初始化的分类都没有统一。在这里,我主要采用Gotw #1的分类方式。大家也可以采用这种方式:

1.默认初始化。

A. 对于用户自定义的类型,调用的是默认构造函数。

注意编译器自动生成的默认构造函数(如果你未定义其他构造函数并且编译器能够为你生成) 不会自动将类里面的成员进行零初始化。注意区分这些函数是trivial还是non-trivial的。如果是trivial的,则其实压根就没有定义,因为trivial的构造函数什么也不做。如果构造函数是non-trivial的,说明 a.自定义类型有基类 或者 b.成员的默认构造函数不是trivial 或者 c.发生虚继承。关于这一点,详见《深度探索c++对象模型》。

B.如果是在局部作用域中的内建类型,例如int。则默认初始化是不进行初始化。

2.直接初始化/值初始化。

即接受相应的参数,然后进行初始化。例如 Widget w(x), 这里的x的类型不是Widget。

注意 new int 不会进行初始化,而new int()会进行零初始化,这也可以认为是一种值初始化。

3.拷贝初始化。

即接受与类本身相同类型的参数,进行拷贝初始化。例如 Widget w(x), 这里的x的类型Widget。

(有时候把拷贝初始化也归为直接初始化)

4.列表初始化(copy list initialization)

列表初始化和初始化列表是两个完全不同的概念,请注意区分。

列表初始化是调用接受std::initializer_list,例如{1,2,3,4},版本的构造函数来进行初始化。这种方式当然是c++98/03里面没有的。

注意在使用{ }来进行初始化时,如果参数能够推导为(甚至通过“隐式转换”)为std::initializer_list,则优先使用接受std::initializer_list版本的构造函数进行初始化。

也就是说“使用{ }来进行初始化”是贪心的。记住这一点尤为重要。


第一部分:C++98/03变量声明 VS C++11/14使用{ }来进行变量声明。

Widget w;  //对w执行默认初始化

          //1.如果Widget是一个用户自定义的类,则调用默认构造函数。
           //2.如果Widget是一个内建类型,例如int。则默认初始化是不进行初始化,也就是值是未定义的。
           //  请谨记《Effective C++》条款04:确定对象使用前已经被初始化。

Widget w();  //什么也不做,这只是一个函数声明,相当于 Widget f ()。
             //还有更复杂的例子, Widget w (begin(), end()),这同样是个函数声明,而不是根据begin()和end()的返回值进行w的初始化。
             //C++标准规定:如果一个表达式能够被当做函数声明,那么它就是函数声明。
             //这种“恼人的分析机制”在《Effective STL》 和《Exception C++》都有相应条款提及,以及更深入的讨论。

Widget w{};    
Widget w = {} //值初始化

             //1.如果Widget是用户自定义类型,则调用默认构造函数。
              //2.如果Widget是内建类型,例如int,则会进行“零初始化”,将w初始化为0。
              //事实上,{}的一个好处就是能够避免上一种情形中的“恼人的分析机制”。

Widget w(x);  //函数声明/拷贝初始化/直接初始化。
             
             //1.如果x是一个类型,这仍然是一个函数声明。
              //2.如果x是一个Widget类型变量,则调用相应的拷贝(copy)构造函数。
              //3.如果x不是一个Widget类型变量,则调用相应的构造函数。


Widget w{x};  //拷贝初始化/直接初始化/列表初始化。

             //1.x不可能是一个类型,所以不会是一个函数声明。
              //2.如果x是一个Widget类型变量,则调用相应的拷贝(copy)构造函数。
              //3.如果x不是一个Widget类型变量,则调用相应的构造函数。
              //  可以看到使用{ }能够避免很多歧义。
              //4.如果定义了接受std::initializer_list的构造函数,则优先调用此版本的构造函数。
              //  注意,接受std::initializer_list的构造函数总是贪心的,哪怕是接受的参数需要隐式转换才能变成
              //  std::initializer_list,而其他构造函数能够完美匹配,也会优先调用此版本。
              //  decltype(x)这里只是表示x的基本类型,不考虑顶层和底层const,还有&和&&。


Widget w = x; //拷贝初始化/直接初始化。

             //1.如果x是一个Widget类型变量,则调用相应的拷贝(copy)构造函数。
              //2.如果x不是一个Widget类型变量,则调用相应的构造函数。
              //  概念上会生成一个临时对象,然后移动或者拷贝过去。但实际上编译器应该会进行优化,省略这一步骤。
              //  如果接受x的单参量构造函数是explicit,则会编译错误!

Widget w = {x}; //拷贝初始化/直接初始化/列表初始化

               //1.如果x是一个Widget类型变量,则调用相应的拷贝(copy)构造函数。
                //2.如果x不是一个Widget类型变量,则调用相应接受x参数的构造函数。
                //3.如果定义了接受std::initializer_list的构造函数,则优先调用此版本的构造函数。
                //  注意,接受std::initializer_list的构造函数总是贪心的,哪怕是接受的参数需要隐式转换才能变成
                //  std::initializer_list,而其他构造函数能够完美匹配,也会优先调用此版本。



Widget w = Widget{x}; 

                    //分两步理解:第一步是采用合适的方式,通过x生成一个右值。可以是直接初始化或者拷贝初始化以及列表初始化。
                    //            第二部是拷贝初始化,将这一个右值移动或者拷贝给w。
                    //编译器实际上通常会把临时对象优化掉。

Widget w = {{}};
Widget w = ({}); //列表初始化

                 //即使{}是贪心的,但如果{}中不包含任何参数,C++标准规定是调用默认构造函数。
                 //如果想用空的initializer_list初始化变量,可以使用这两种方式。
 
  

 
  

int w {1.1};    
int w = {1.1};  //{}不允许窄型转换,按标准是不能通过编译的。
                //但我是用G++编译器只会给出警告,而VS 2015会编译失败。
                //具体行为可能取决于你的编译器类型或者版本。 

第二部分:结合使用{ }和auto来进行变量声明。

auto w = x; //拷贝初始化。

            //通过拷贝构造函数或移动构造函数,声明w是一个和x同样类型的变量。

auto w = {x}; //列表初始化。

              //声明w是一个std::initializer_list类型的变量,initializer_list包含x一个元素。

auto w = {x,y};    //列表初始化
                   //如果x和y同类型,则声明w为此std::initializer_list类型的变量。
                   //如果x和y不同类型,则会编译错误!  
auto w{x};         //这个。。。
                   //我在G++和VS 2015上面都进行尝试了尝试,结果却不一样。
                   //在C++11/14标准中,会生成一个std::initializer_list类型的变量,这是G++的行为。
                   //但是有一个新的提案N3922被采纳进了C++17,其规定w是x的类型,这是VS 2015的类型。

                  //我的建议是,不要使用这种方式定义变量。

auto w{x,y};       //这个。。。
                   //同样在G++和VS 2015上面都进行尝试了尝试,结果不一样。
                   //在C++11/14标准中,如果x和y类型一样,会生成一个std::initializer_list类型的变量,否则编译失败,这是G++的行为。
                   //但提案N3922被采纳进了C++17,其规定这种方式编译失败!这是VS 2015的行为。

                  //我的建议是,不要使用这种方式定义变量。

auto w = Widget{x}; 
                    //分两步理解:第一步是采用合适的方式,通过x生成一个右值。可以是直接初始化或者拷贝初始化以及列表初始化。
                    //            第二部是拷贝初始化,将这一个右值移动或者拷贝给w。
                    //编译器实际上通常会把临时对象优化掉。


auto w = (x, y); 
                    //额,这个只是一个玩笑。。。
                    //不过不要以为上面的代码不能够通过编译,也不要以为这是python里面的元组。
                    //上面的类型是 decltype(y),如果不理解,回忆一下逗号运算符。


关于N3922提案的问题,详见:

http://stackoverflow.com/questions/25612262/why-does-auto-x3-deduce-an-initializer-list

http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2014/n3922.html


上面列举了这么多情形,到底要怎么记呢?

下面是我总结的。

如果没有使用auto:

  • 如果没有{ },则还是C++ 98/03的标准。
  • 如果是空的{ },则调用的是自定义类型的默认构造函数,或者对于内建类型进行零初始化。空的{ }不会调用接受initializer_list的构造函数,除非是显式通过{ {} }或者({ })。
  • 如果不是空的{ }:
    • 如果有接受initializer_list为参数的构造函数,则优先调用此版本。哪怕需要隐式类型转换才能调用,而有其他版本的构造函数能够完全匹配,也会优先调用接受initializer_list为参数的构造函数。
    • 如果没有接受initializer_list为参数的构造函数,则按照正常函数匹配规则,寻找最为匹配的构造函数。
    • 注意上述跟有没有“=”号(有“=”时{ }在右边)没有关系。

如果有使用auto:(注意auto一定要求初始化)

  • 请记得一定使用 auto w = expr 或者 auto w = Type{init},即带“=”号的风格,这也是Gotw里面强烈推荐的,应该使用这两种来定义所有变量。而对于 auto w{x} 这种情形,现在标准还在讨论中,C++11/14 w是为initializer_list,但C++17中w的类型应该是 decltype(x) 了。
  • 如果在 auto w = expr 中,expr是一个{ },则w很明显,也是一个initializer_list。
  • 因为{ }不允许窄型转换,当时确实需要时,可以使用 auto w = Type(init)。


第三部分:使用auto来进行变量定义。

关于使用auto来进行变量的定义,其实关键是在于两点:

1. auto的类型推导规则和模板参数的推导规则完全一样,即会忽略顶层的const 以及引用 & 和 &&。

2. 除第一点外,auto还能够推导initializer_list,例如 auto x = {x, y};


我不说声明而说定义的原因是auto总要求初始化,这不同于传统的方式,所以也更能保证代码的正确性。


关于使用auto来进行变量的定义,《Effective Modern C++》和Gotw的92到94条款都详细进行了讨论,并给出了一系列的指导原则。

尤其在Gotw #94中,提出了“Almost Always Auto”风格,即3A风格。Meyers的条款05:请使用auto来进行显式的变量声明,也是推荐使用auto来进行变量定义。


auto 的作用要比你最初看到时的要深刻得多,两位大师都用了至少3条条款来进行讨论。Meyers的关于auto的几个条款网上貌似已经有翻译,大家可以上网搜搜。但是Gotw的#92到#94我并没有看到翻译版本,英文还凑合的同学可以直接看原文。兴许未来,博主我也有兴趣去翻译一下。


关于auto,本文只做以上概述性的总结,两位大师分别都用了至少3个条款来详细阐述。我总结肯定比不上人家大师,而且内容太多。


兴许,待续。。。。。。


你可能感兴趣的:(Modern,C++)