Xander's Wiki


  • Home

  • Tags

  • Categories

  • Archives

  • Search

6.当auto推衍类型错误时,使用显式初始化

Posted on 2018-06-26 | In Effective Modern C++

前言

 
在Item5中曾经提及,使用auto声明变量相较于显式类型声明有许多优势,但有时也会带来问题。


问题实例

 
举例而言,假若我们当前有一个函数,其形参为一个Widget切返回一个std::vector<bool>,每一个bool表征当前Widget是否提供某项功能:

1
std::vector<bool> features(const Widget& w)

进一步地,我们假定bit5代表Widget是否具备权限,因此我们可能会写出如下程序:
1
2
3
Widget w;
bool highPriority = features(w)[5];//is w high priority?
processWidget(w, highPriority);//process w in accord with its priority

上述代码将会运行正常,但如果我们使用auto代替显式的类型声明:
1
2
auto highPriority = features(w)[5];
processWidget(w, highPriority);

上述代码仍然能够通过编译,只是其行为不可预料。原因很简单,vector<bool>返回的并不是一个bool类型,而是一个代理类对象的引用,从而导致上述函数无法被正确运行(详见Effective STL Item18)。
在原始程序中,highPriority发生了一次隐式类型转换,而auto只会如实推衍类型(一个reference-to-bit),不会发生任何类型转换。除此之外,我们在进一步剖析上述程序时会发现,feature(w)生成了一个临时的Vector,highPriority作为其内部元素的一个引用,在语句结束后不可避免地成为了空悬指针,这也就是函数行为不可预料的原因所在。


proxy class

 
不仅仅vector<bool>存在着代理类的情况,标准库中的智能指针类型在某种意义上也是负责管理内存资源的代理类。事实上,设计模式中的代理模式在程序设计中十分流行,有些代理类对于用户而言是显然的,例如std::shared_ptr与std::unique_ptr,但有一些被设计为对用户不可见,例如std::vector<bool>::reference。

除此之外,某些C++库中的class使用了一种名为”expression template”的技术,这项技术的目的主要在于提高数学计算效率。举例而言,现有矩阵计算表达式如下:

1
Matrix sum = m1 + m2 + m3 + m4;

如果使用了”expression template”技术,那么operator+返回的并非是计算结果,而是一个代理类,例如Sum<Matrix,Matrix>之类(此技术的详细说明详见More Effective C++ Item17)。operator=执行了一次隐式类型转换,但如果我们采用auto声明变量,恐怕会得出sum的类型为Sum<Sum<Sum<Matrix, Matrix>,Matrix>, Matrix>。

隐性代理类与auto配合不甚默契,因为它们的生存期往往十分短暂(语句结束即完成析构),因此创建这些类型的变量往往会违反基本设计原则,其往往带来未定义后果。


解决方案

1
auto someVar = expression of "invisible" proxy class type;

这种写法固然不可取,但如何避免出现这类表达式呢?

首先,我们应当学习如何找到隐性代理类。尽管它们难以被开发者所察觉,但库通常会记录下它们的行为,你对库越熟,那么你越不可能忽视这些代理类的使用。即使文档中没有给出介绍,头文件也会忠实地反应出代理类的使用情况,例如vector<bool> operator[]声明:

1
2
3
4
5
6
7
8
9
10
namespace std { // from C++ Standards
template <class Allocator>
class vector<bool, Allocator> {
public:
…
class reference { … };
reference operator[](size_type n);
…
};
}

我们可以一目了然地发现vector<bool>返回了一个代理类。

在实际开发过程中,许多人只有在他们试图追踪编译问题或不正确的单元测试结果时才会发现使用代理类的存在。但不管你是怎么找到它们的,即使auto推衍得到了某个代理类类型,但我们仍然没有放弃使用auto的理由。auto本身并不是问题,问题是auto无法推衍出我们想要的类型,因此解决方案很简单:我们强制auto推衍为正确的类型,这种手法被本书作者称为“the explicitly typed initializer”。 idiom”。其基本表现形式如下:

1
2
auto highPriority = static_cast<bool>(features(w)[5]);
auto sum = static_cast<Matrix>(m1 + m2 + m3 + m4);


总结

  1. 隐性代理类可能会导致auto推衍出“错误”的类型。
  2. 我们可以使用the explicitly typed initializer加以修正。

5.用auto代替显式类型声明

Posted on 2018-06-26 | In Effective Modern C++

前言

 
通过使用auto技术,我们省下了打字的时间,但可能会带来一些手动声明类型所不会造成的错误或性能问题。作为C++开发者,我们应当尽力掌握auto以期获取正确结果。


问题实例

 
在C++11中未曾出现之前,声明一个变量可能会出现三种令人厌恶的情况:

  1. 忘记初始化
    1
    int x;//x's value depends on the context
  2. 类型名过长
    1
    2
    3
    4
    5
    6
    7
    template<typename It>
    void dwim(It b, It e){
    while (b != e) {
    typename std::iterator_traits<It>::value_type currValue = *b;
    …
    }
    }
  3. 变量类型不确定(闭包),只有编译器知道具体类型

C++11引入的auto技术完美解决了上述三点问题。使用auto声明的变量会从初始化语句中推断类型,因此它们必然会被初始化,并且无需写出繁复的类型名称:

1
2
3
4
5
6
7
8
9
10
11
12
13
auto x = 0;//must be initialized

template<typename It> // as before
void dwim(It b, It e){
while (b != e) {
auto currValue = *b;
…
}
}

auto derefUPLess = []//only compiler know return closure's type
(const std::unique_ptr<Widget>& p1,const std::unique_ptr<Widget>& p2)
{ return *p1 < *p2; };

C++14将这一特性再次升级,现在lambda表达式内部也可以使用auto了:
1
2
auto derefLess = [](const auto& p1,const auto& p2)
{ return *p1 < *p2; }; // to by anythin


std::function

 
有人认为我们没有必要使用auto来声明一个持有closure的变量,因为我们可以使用std::function对象。前提是你了解function object。

std::function是C++11标准库中的一个模板,其类似于一个通用函数指针。函数只能指向一个函数,但std::function可以指向任何一个可调用对象。当你声明一个函数指针时你必须指出其指向函数的签名,function object亦是如此,其使用大致如下所示:

1
2
3
4
//C++11 signature for std::unique_ptr<Widget> comparison function
bool(const std::unique_ptr<Widget>&, const std::unique_ptr<Widget>&)

std::function<bool(const std::unique_ptr<Widget>&,const std::unique_ptr<Widget>&)> func;

由于lambda表达式生成了可调用对象,因此closure可以保存在std::function对象之中,因此我们在不使用auto技术的前提下可以将derefUPLess函数声明为:
1
2
3
std::function<bool(const std::unique_ptr<Widget>&,const std::unique_ptr<Widget>&)>
derefUPLess = [](const std::unique_ptr<Widget>& p1,const std::unique_ptr<Widget>& p2)
{ return *p1 < *p2; };

即使不考虑语句冗长与类型反复撰写,使用std::function与使用auto也截然不同。一个持有closure由auto声明的变量与闭包具备同样的类型,因此其只需要与闭包一致的内存即可。但持有closure的function对象是一个template的实例化,因此其所占有的内存与签名式相关,但该内存可能并不足以存放closure,那么此时function将会分配heap内存以存储closure。这一特性导致function对象往往比auto对象使用更多的内存。此外,由于限制内联及函数简介调用,由一个function对象触发closure往往会慢于auto对象。

总之,function对象往往占用较多的内存且慢于auto对象,并且可能会产生内存异常。使用function也没有auto简洁与直观。


auto的其他优点

 
auto还有一个好处是可以避免”type shortcuts”,比如你可能看过甚至自己写过这样的程序:

1
2
3
std::vector<int> v;
...
unsigned sz = v.size();//其类型不应为unsigned,这里只是因为开发者偷懒

有人认为unsigned类型与vector::size_type一致,但实际上这是错误的,在64位电脑中vector::size_type为64位,而unsigned只有32位,如果使用auto则可以避免这一问题。

又有实例如下:

1
2
3
4
5
std::unordered_map<std::string, int> m;
…
for (const std::pair<std::string, int>& p : m){
… // do something with p
}

这个程序看起来并没有什么不对(如果你没有读过Effective STL或类似书籍的话),但实际上,pair的类型并非pair<std::string,int>,而是pair<const std::string,int>。由于类型不一致,编译器将会生成一个临时对象绑定到p上,并且在每一次循环结束后析构该对象。这不仅仅付出了构造、析构的成本,你所有的操作最终都将作用于临时对象之上,对m中的元素并没有发挥任何作用,但使用auto则可避免这一问题:
1
2
3
for(const auto& p:m){
...
}


auto可读性

 
当然,auto并非尽善尽美,原因在auto作类型推衍的根基在于其初始化表达式,而这可能会带来非你所想的类型推衍结果,在Item2与Item6中介绍了它们的产生原因与消除方案,因此本节不再赘述。接下来要讨论的是auto所带来的可读性问题。

首先我们需要明确的是,auto只是一个可选项而非强制性措施。根据以往的编程经验,代码应当尽可能在简洁的同时做到可维护,但auto并没有破坏这些原则,实际上无论是静态语言(C#,VB等),还是动态语言(Haskell,F#等),类似的功能早已出现,类型推衍与传统编程原则并没有任何冲突。

有些开发者会担心使用auto将导致他们无法快速了解某变量的类型,但实际上IDE会显示出这些变量的类型,并且在许多情况下,我们只需要知道对象的大概类型即可(例如只需要了解其是一个指针、迭代器、容器,而不需要知道具体类型)。一个好的变量名应当能够正确地反映出大致数据类型。

事实上,显式类型说明几乎没有特别明显的优点,它可能会引入微妙的错误,又或者会对效率产生一定影响,甚至二者兼而有之。此外,使用auto完成声明的对象会在其初始化表达式发生改变时自动更改类型,这对重构有着莫大帮助。


总结

  1. 使用auto声明对象意味着必须初始化,并且能够规避类型匹配错误与可移植性、效率低下等问题。在此基础之上,auto还能够简化重构过程,节约大量不必要的类型声明代码。
  2. auto可能存在一些缺陷,这些在Item2与Item6中有所描述。

4.了解如何查看类型推衍结果

Posted on 2018-06-25 | In Effective Modern C++

前言

 
一般来说有三个阶段可以进行类型推衍结果查看:撰写期、编译期、运行期。


IDE

 
IDE通常可以在完成解析的前提下帮你判断当前类型推衍所得到的结果,但对于较为复杂的类型可能会失效。


编译期诊断

 
最有效的让编译器显示其推衍结果的方法是故意错用这些类型,编译器所给出的错误信息将清楚地告诉你哪里发生了错误。

问题实例

 
现有需要作类型推衍的变量x、y:

1
2
3
const int theAnswer = 42;
auto x = theAnswer;
auto y = &theAnswer;

首先我们需要定义一个类以展示类型:
1
2
template<typename T>
class TD;//Type Displayer

任何试图实例化该类的操作都将引发错误,因为TD并没有定义。如果需要查看x、y的推衍结果,我们只需要用其类型去实例化TD:
1
2
TD<decltype(x)> xType;
TD<decltype(x)> yType;

以上操作会产生报错信息如下所示:
1
2
error: aggregate 'TD<int> xType' has incomplete type and cannot be defined
error: aggregate 'TD<const int *> yType' has incomplete type and cannot be defined

至此完成了编译期类型判别。


运行期输出

 
有人会想到使用typeid与std::type_info::name来输出对象格式:

1
2
std::cout << typeid(x).name() << '\n';
std::cout << typeid(y).name() << '\n';

但这种方法有一个缺陷,因为typeid必须作用于一个可以生成std::type_info对象的对象x或y,且type_info对象必须要有一个返回const char*的name成员函数。

对std::type_info::name的调用并不保证返回能够令你读懂的消息,据本书作者所言,微软公司提供的反馈较为直白。

即使已经得到了正确的类型,也可能会有一些其他问题存在。举例而言:

1
2
3
4
5
6
7
8
9
10
template<typename T>
void f(const T& param);

std::vector<Widget> createVec();

const auto vw = createVec();

if(!vw.empty()){
f(&vw[0]);
}

为了在运行期明确T与param的类型,我们可以定义f如下所示:

1
2
3
4
5
6
template<typename T>
void f(const T& param){
using std::cout;
cout << "T = " << typeid(T).name() << std::endl;
cout << "param = " << typeid(param).name() << std::endl;
}

微软编译器将给出结果如下:
1
2
T = class Widget const *
param = class Widget const *

显然,这并非正确的结果,假若T为int型,那么param必然为const int&才对。发生这样错误的原因在于std::type_info::name规格认为模板参数以值传递的方式传递给模板函数,这导致了referenceness与constness、volatileness被丢失。

此外,IDE所推衍得到的类型也未必可靠,至少有时候帮助不大,例如以下这个类型T:

1
2
const std::_Simple_types<std::_Wrap_alloc<std::_Vec_base_types<Widget,
std::allocator<Widget> >::_Alloc>::value_type>::value_type *

但IDE却推断param的类型是:
1
const std::_Simple_types<...>::value_type *const &

中间的…省略号省略了所有关于T类型。


Boost

 
IDE与type_info::name可能会出错,但Boost不会出错,并且Boost库支持跨平台运行,使用它和使用标准库一样方便,以下将给出使用Boost完成类型推衍的过程:

1
2
3
4
5
6
7
8
9
10
#include <boost/type_index.hpp>

template<typename T>
void f(const T& param){
using std::cout;
using boost::typeindex::type_id_with_cvr;

cout << "T= " << type_id_with_cvr<T>().pretty_name() << endl;
cout << "param = "<< type_id_with_cvr<decltype(param)>().pretty_name() << '\n';
}

它的工作方式是函数模板boost::typeindex::type_id_with_cvr接受一个类型参数并且在推衍结果中不移除const,volatile或reference(with_cvr)。


总结

  1. 你可以使用IDE、错误信息、Boost库来查看类型推衍。
  2. 上述工具可能会产生错误,因此熟练Item1-3所描述的法则才是最重要的。

3.了解decltype技术

Posted on 2018-06-25 | In Effective Modern C++

前言

 
decltype是一项奇特的技术,它能够告诉你某个名称或表达式的类型,尽管有时候返回的结果令你抓耳挠腮。


decltype的使用

 
我们首先从最简单的情况开始分析,在此情况中decltype只会鹦鹉学舌般反馈类型:

1
2
3
4
5
const int i =0;//decltype(i) is const int
bool f(const Widget&w);//decltype(w) is const Widget&,decltype(f) is bool(const Widget&)
if(f(w))//decltype(f(w)) is bool
vector<int> v;//decltype(v) is vector<int>
if(v[0]==0)...//decltype(v[0] is int&

看起来一切都很正常。


decltype与返回值

在C++11中,decltype主要用于声明函数返回值依赖于其类型参数的函数模板。举例来说,我们的函数可能会操作一个带有索引的容器,在返回容器内部元素的值之前,我们需要验证一下使用者的身份,此函数的返回值类型显然应当与operator[]的返回值类型一致,此时应当使用decltype。

有人认为在容器类中,operator []总是返回T&。对于deque来说这总是成立的,但对于vector来说,vector<bool>则并非返回一个bool&,而是一个新对象。(代理类,这一问题详见Effective STL Item18及More Effective C++ Item30),这里我们强调的是,一个容器的operator []返回类型不仅仅与其元素类型有关,也与容器自身有关。

下面的实例展示了decltype的用法,它将在后期被进一步改进:

1
2
3
4
5
template<typename Container,typename Index>
auto authAndAccess(Container&c,Index i)->decltype(c[i]){
authenticateUser();
return c[i];
}

函数名前的auto与类型推衍毫无关联,它只是表示此处我们使用了C++11的尾置返回类型(C++ Primer P604)。我们的返回类型取决于c、i,因此我们不可能在没有遇到它们之前声明返回值类型,这即是尾置返回类型的特性与优势所在。

C++11允许使用auto推衍单语句lambda的返回值类型,C++14将其扩展至所有多语句lambda与函数。这意味着在刚才的实例中我们可以不使用尾置返回类型,仅使用auto来完成类型推衍:

1
2
3
4
5
template<typename Container,typename Index>
auto authAndAccess(Container&c,Index i){//not quite correct
authenticateUser();
return c[i];//type deduction take place
}

在Item2中我们曾经说明auto用于函数返回值类型推衍时采用template function推衍法则,这一特点造成了上文实例可能推衍失败的可能,原因在于容器类operator[]的返回类型几乎都是T&,但template function在作类型推衍时将忽略referenceness,如下所示:
1
2
std::deque<int> d;
authAndAccess(d,5) = 10;//can not compile

理论上来说,等式左侧应当是一个int&,但实际上由于template function推衍法则,等式左侧被推衍为int。由于该int是一个函数的返回值,所以它是一个右值,这直接导致了本语句无法编译。


C++14中的decltype(auto)

 
为了保证函数功能正常,我们需要使用decltype来推衍函数返回值类型.C++14引入了decltype(auto)类型说明符,其功能为:auto用以说明当前类型需要被推衍,而decltype则代表了类型推衍所需要采用的法则,修改之后的authAndAccess:

1
2
3
4
5
template<typename Container,typename Index>
decltype(auto) authAndAccess(Container& c,Index i){//still require refinement
authenticateUser();
return c[i];
}

decltype不仅仅可以用来作为返回值类型说明符,也可以用于在初始化语句中定义对象,例如:
1
2
3
4
Widget w;
const Widget& cw = w;
auto myWidget1 = cw;//auto type deduction: myWidget1's type is Widget
decltype(auto) myWidget2 = cw;//decltype deduction:myWidget2's type is const Widget&


authAndAccess的最终修正

 
我们首先回顾一下authAndAccess的声明:

1
2
template<typename Container, typename Index>
decltype(auto) authAndAccess(Container& c, Index i);

容器对象的传递采用reference-non-const形式,如此即可保证用户可通过该函数修改容器内部元素。这种传递方式限制了rvalue的传入,因为rvalue无法被绑定至lvalue引用(除非是lvalue-reference-to-const)。

诚然,传递一个右值容器对象是一种罕见的情况。一个右值容器对象必然是一个临时对象,其在authAndAccess调用完成后必将销毁,那么返回其内部元素的引用也必将造成空悬。但这种情况未必不会发生,例如客户可能希望拷贝一个临时容器中的元素:

1
2
std::deque<std::string> makeStringDeque();//Factory function
auto s = authAndAccess(makeStringDeque(),5);//can't compile

为了解决这个问题,有些开发者也许会想到重载,不过那样我们需要撰写两个函数,这是没必要的,Item24将为我们介绍的universal reference可以同时绑定lvalue与rvalue:
1
2
template<typename Container, typename Index>
decltype(auto) authAndAccess(Container&& c, Index i);

在这个template function中我们对容器类型一无所知,这也直接导致了我们对其索引对象的类型(Index)不甚明确。对未知类型对象采用值传递的方式可能会导致性能下降、slicing等诸多问题,但有时确实存在着pass-by-value的必要,因此我们坚持对Index对象采用pass-by-value。最终,我们采用Item25所建议的std::forward对函数作出修正:
1
2
3
4
5
template<typename Container, typename Index>
decltype(auto)authAndAccess(Container&& c, Index i){
authenticateUser();
return std::forward<Container>(c)[i];
}


decltype的特殊情况

 
将decltype应用于某个名称将导致对此名称的类型推衍,名称是一种左值表达式,不过这不会影响decltype的行为。但是对于比名称更为复杂的左值表达式,decltype将保证类型推衍结果必然是一个左值引用。一般来说这不会造成多大影响,因为左值表达式自带一个引用符,举例来说,一个返回左值的函数即返回左值引用。

请注意下面的例子:

1
int x = 0;

x是变量的名称,因此decltype(x)的结果是int。但如果我们用一个小括号将x包起来形成(x),它就变成了一个左值表达式,那么decltype((x))的结果则会变为int&,但更麻烦的还在后面,有时候这种写法会直接导致崩溃,考虑下面两个函数
1
2
3
4
5
6
7
8
9
decltype(auto) f1(){
int x = 0;
return x;//return int
}

decltype(auto) f1(){
int x = 0;
return (x);//return int& to a local variable!
}

由此可见,表达式类型在一定程度上可能会影响decltype(auto)的推衍结果,Item4将会介绍确保每一次推衍结果都我们所愿的方法。


总结

  1. decltype在作类型推衍时几乎总是不发生任何改变。
  2. 对于并非名称的左值表达式,decltype几乎总是返回T&。
  3. C++14支持decltype(auto),其表示以decltype法则作类型推衍。

2.了解auto技术

Posted on 2018-06-23 | In Effective Modern C++

前言

 
auto的类型推衍法则除了一个特例之外与template function保持一致。


问题实例

 
在上一节中,template function的类型推衍有实例如下:

1
2
3
4
template<typename T>
void f(ParamType param);

f(expr);

在函数调用过程中,编译器将使用expr推衍T与ParamType的类型。

当开发者使用auto来声明一个变量时,auto将扮演T在template中的角色,同时,type specifier(类型说明符)则类似于ParamType,如下所示:

1
2
3
auto x = 27;//type specifier为auto
const auto cx = x;//type specifier为const auto
const auto&rx = x;//type specifier为const auto&

为了推衍出x、cx、rx的类型,编译器会像调用fun template那样执行操作:
1
2
3
4
5
6
7
8
9
10
11
template<typename T>
void func_for_x(T param);//conceptual template
func_for_x(27);//conceptual call

template<typename T>
void func_for_cx(const T param);
func_for_cx(x);

template<typename T>
void func_for_rx(const T& param);
func_for_rx(cx);


问题剖析

 
在template function类型推衍中我们根据ParamType的特点将情况分为了三种,使用auto的变量声明表达式与之类似,根据type specifier也分为了三种情况:

  1. type specifier是一个pointer或一个reference,但不是一个universal reference;
  2. type specifier是一个universal reference;
  3. type specifier既不是pointer也不是reference;

显然,前文所述的实例分为对应为:

1
2
3
auto x = 27;//case 3
const auto cx = x;//case 3
const auto& rx = cx;//case 1

Case2有推衍实例如下:
1
2
3
auto&& uref1 = x;//x is int and lvalue,so uref1's type is int&
auto&& uref2 = cx;// cx is const int and lvalue,so urfe2's type is const int&
auto&& uref3 = 27;//27 is int and rvalue,so urfe3's type is int&&

在Item1中描述了数组名与函数名在遇到非引用type specifier时会退化为指针的情况,这一特性在auto类型推衍过程中也同样适用:
1
2
3
4
5
6
7
const char name[]="R.N.Briggs";
auto arr1 = name;//arr1's type is const char*
auto& arr2 = name;//arr2's type is const char(&)[13]

void someFunc(int,double);
auto func1 = someFunc;//func1's type is void(*)(int,double)
auto& func2 = someFunc;//func2's type is void(&)(int,double)


auto与template function类型推衍的不同之处

 
如果我们想要声明一个值为27的int型变量,C++98为我们提供了两种方式:

1
2
int x1 = 27;
int x2(27);

C++11引入了列表初始化的概念,因此增加了两种声明方式:
1
2
int x3 = {27};
int x4{27};

我们将会在Item5中说明,使用auto来声明变量比指定类型更佳,那我们试着把上述声明改为:
1
2
3
4
auto x1 = 27;
auto x2(27);
auto x3 = {27};
auto x4{27};

上述声明均能够通过编译,只是其含义略有差别,前两个声明式仍然保持着声明一个值为27的int变量的含义,但后两个却发生了变化:
1
2
auto x3 = {27};//x3's type is std::initializer_list<int>,value is {27}
auto x4{27};//ditto

造成这一变化的根本原因在于auto的推衍法则:当被声明的变量值被大括号包围时,其变量类型会被推衍为std::initializer_list,当包括中的类型不一致时,类型推衍将无法完成:
1
auto x5 = {1,2,3.0};//error can not deduce

需要明确的是在上述语句中发生了两种类型推衍,第一种类型推衍将x5推衍为initializer_list类型(因为其值被大括号包围),但initializer_list本身是一个template,所以我们需要继续执行类型推衍。

这种将大括号初始化推衍为initializer_list是auto与template function唯一不同的地方,后者不允许这种推衍方式:

1
2
3
4
5
6
auto x = {11,23,9};//x's type is initializer_list<int>

template<typename T>
void f(T param);

f({11,23,9});//error!can't deduce type for T

但是如果你将template function中的参数声明为std::initializer_list,只是其元素类型T不明确的话,template function可以完成推衍:
1
2
3
4
template<typename T>
void f(std::initializer_list<T> param);

f({11,23,9});//T's type is int

综上,auto与template function唯一的推衍区别在于:auto总是假定一个大括号初始化列表代表着一个initializer_list,而template function不作这种假设。


C++14中的auto

 
在C++14中,auto还可以用来表征函数的返回值类型,并且lambda表达式也经常使用auto来声明参数。不过这两种auto的使用方式遵从template function类型推衍法则,而非auto推衍法则,因此返回一个初始化列表的函数将无法用auto推衍:

1
2
3
auto createInitList(){//error,can't deduce type for {1,2,3}
return {1,2,3};
}

lambda表达式中亦是如此:
1
2
3
4
5
std::vector<int> v;
...
auto resetV = [&v](const auto& newValue){ v = newValue;}
...
resetV({1,2,3});//error,can't deduce type for {1,2,3}


总结

 

  1. auto的推衍法则类似于template function,唯一的不同在于auto会假定大括号初始化列表为std::initializer_list,template function则不会。
  2. 对函数返回值或lambda表达式参数使用auto时遵从template function类型推衍法则,而非auto自己的法则。

1.了解模板类型推衍

Posted on 2018-06-20 | In Effective Modern C++

前言

 
如果开发者在不了解某系统运行机制的前提下仍能高效地使用它,他们往往会给予该系统极高的赞美。由此说来,C++中的模板类型推衍无疑是一项巨大的成功,数以万计的开发者轻松获取了满意的结果,尽管他们不得不为了描述这些经由推衍得到的类型而做出相当大的努力。

如果你就是上述开发者中的一员,迎接你的是一个好消息与一个坏消息。好消息是模板类型推衍技术是auto技术的基础,如果你对于模板类型推衍感到满意,那么想必你也会认为auto老少咸宜。坏消息是auto技术中所使用的类型推衍技术相较于template可能不够直观。为了清楚地了解auto,我们首先最好认知一下模板类型推衍。


问题实例

 
考虑以下函数模板及其调用:

1
2
3
4
template<typename T>
void f(ParamType param);

f(expr);

在编译期间,编译器会使用expr去推衍T、ParamType。这两者往往存在一定的差异,原因在于ParamType可能会带有一定的修饰,例如const或者reference之类。举例而言,如果该函数模板被声明和使用如下:
1
2
3
4
5
template <typename T>
void f(const T& param);

int x=0;
f(x);

那么T将被推衍为int类型,而ParamType将被推衍为const int&。

认为实参类型类型等价于T的类型是一种十分自然的想法,就像刚才的T为int一样,但很多时候情况并不是这样,事实上,T的类型并不完全取决于expr的类型,也与ParamType的形式有关。以下将给出三组案例:

  1. ParamType是一个pointer或reference,但其非universal reference。(universal reference将在Item24中给出解释,这里只需要明确其并非左值引用或右值引用)
  2. ParamTyp是一个universal reference。
  3. ParamType既不是pointer也不是reference。

Refernece or Pointer,and not a universal reference

 
这种情况是最简单的情况,在此情况下,类型推衍遵循以下原则:

1. 如果expr的类型是一个refernce,则忽略refernece部分。
2. 根据expr与ParamType的类型完成T类型推衍

举例而言,现有声明与应用如下所示:

1
2
3
4
5
6
7
8
9
10
11
// function template declartion
template <typename T>
void f(T& param);
//variable declaration
int x =27;
const int cx = x;
const int& rx =x;

f(x);//T is int,param's type is int&
f(cx);//T is const int,param's type is const int&
f(rx);//T is const int,param's type is const int&

需要注意的是第二和第三个函数调用,由于cx与rx代表常量,因此T是一个const int,param的类型是const int&。当你试图传递一个常量给引用参数时,编译器会认为对象仍然需要保持不可变性,因此param会成为一个reference-to-const。我们可以认为,const对象传递给引用类型的形参是安全的,编译器会推衍出T带有const属性。

在第三个函数调用中,rx本身是一个reference,而T被推衍为non-refernce,这是由于rx的reference属性在类型推衍中被忽略所致。

上述的三个实例都是左值引用,但实际上右值引用的类型推衍并没有什么不同,非要说的话就是只有右值实参才能被赋予右值形参。

如果我们将template function的形参由T&改为const T&,事情会发生一点点小小的变化。cx与rx的const属性仍然得到了关注,但由于我们已经认定param是一个reference-to-const,因此T将被推衍为int类型,即:

1
2
3
4
5
6
7
8
9
10
template <typename T>
void f(const T& param);

int x = 27;
const int cx = x;
const int& rx =x;

f(x);//T is int,param's type is const int&
f(cx);//T is int,param's type is const int&
f(rx);//T is int,param's type is const int&

当然,这里rx的reference属性也被忽略了。

如果param是一个pointer或pointer-to-const,那么类型推衍结果几乎没什么变化:

1
2
3
4
5
6
7
8
template<typename T>
void f(T* param);

int x = 27;
const int *px = &x;

f(&x);//T is int,param 's type is int*
f(px);//T is const int,param's type is const int *


ParamType is a Universal Reference

 
在上一案例中,一切推衍都十分自然。但当template以universal reference作为参数时,推衍法则将变得不再显然。某些parameters被声明为右值引用,但当左值传入时,其表现的结果将不再类似于右值引用。关于universal reference的详细介绍将在Item24展开,接下来将给出本案例的推衍法则:

  1. 如果expr是一个左值,那么T与ParamType都将被推衍为左值引用。这里存在两点不同寻常之处,首先,只有在此种情况下template才会将T推衍为引用类型;其次,尽管ParamType被声明为右值引用,但最终其推衍类型是一个左值引用。
  2. 如果expr是一个右值,则遵照案例1中的法则执行。

有问题实例如下:

1
2
3
4
5
6
7
8
9
10
11
template<typename T>
void f(T&& param);

int x= 27;
const int cx = x;
const int& rx =x;

f(x);//x is lvalue,T is int&,param's type is int&
f(cx);//cx is lvalue,T is const int&,param's type is const int&
f(rx);//rx is lvalue,T is const int&,param's type is const int&
f(27);//27 is rvalue,T is int,param's type is int&&

在Item24中将会详细解释此处的类型推衍法则原理。在Universal reference案例中,关键点在于实参是左值还是右值会导致不同的推衍原则被执行,这在non-universal reference中不会出现。


ParamType is Neither a Pointer nor a Reference

 
这种情况意味着执行值传递——产生了一个对象的copy,这一现象产生了本案例下的类型推衍法则:

  1. 如果expr为reference,则忽略其reference属性。
  2. 如果在忽略reference属性后,expr还留有const或volatile属性,则一并忽略。

有实例如下:

1
2
3
4
5
6
7
8
9
10
template <typename T>
void f(T param);

int x = 27;
const int cx = x;
const int& rx = x;

f(x);//T is int,param's type is int
f(cx);//T is int,param's type is int
f(rx);//T is int,param's type is int

这里需要注意的是,即使cx与rx代表常量,param也并非const。我们应当明确,param是一个与cx或rx无关的对象,它由copy得到,这很好地解释了为什么param不会具备expr所带有的constness与volatileness。不过需要说明的是,constness或volatilness只在pass-by-value中会被忽视,pass-by-pointer或pass-by-reference则会保留其特性。最后,让我们看看以下这种情况:

1
2
3
4
5
template <typename T>
void f(T param);

const char* const ptr = "Hello,world!";
f(ptr);//T is const char*, param's is const char*

在本实例中,我们需要明确的是param是一个ptr的拷贝,因此底层const属性被丢失(bitwise copy),而顶层const则得到了保留。


Array Arguments

 
尽管我们经常在实际使用中将数组类型与指针混为一谈,但在类型推衍中它们存在一定的区别。我们首先复习一下数组名与指针的关系:

1
2
const char name[] = "J.P.Briggs";//name's type is const char[13]
const char* ptrname = name;//array decays to pointer

显然,ptrname经由name得到了初始化,尽管它们的类型并不相同,但由于退化原则(array-to-pointer),上述代码得以被编译。不妨试想,如果一个数组以pass-by-value方式传递给template,将会如何?
1
2
3
4
template <typename T>
void f(T param);

f(name);

为了解决这个问题,我们先从一个更加基础的问题开始回答:当数组作为一个函数的参数时会发生什么?

1
void myFunc(int param[]);

此处的形参尽管被声明为数组,实际上编译器会将其视为一个指针,因此,myFunc声明式等价于
1
void myFunc(int* param);

数组名与指针等价的规则起始于C语言,这一设定也被C++继承,这直接导致了开发者产生了数组等价于指针的错误观念。基于这一规则,我们在类型推衍中有设定如下:当一个数组以pass-by-value的形式被传递给template function时,其类型总是被推衍为指针:
1
f(name);//name is array,T deduced as const char*

但是我们需要明确,尽管函数的形参不能够真正地声明为数组类型,但其可以声明为数组的引用,因此假若我们有template function声明如下:
1
2
template <typename T>
void f(T& param);

我们再次传入数组,
1
f(name);

此时T的类型将是一个真正的数组。数组类型包含了数组的长度,因此在本例中,T将会被推衍为const char[13],ParamType的类型为const char(&)[13]。这一特性也保证了模板可以推衍出数组包含元素的个数:
1
2
3
4
5
//return size of an array as complie-time constant
template<typename T,std::size_t N>
constexpr std::size_t arraySize(T (&)[N]) noexcept{
return N;
}

constexpr意味着编译期间即可得到变量值,因此constexpr修饰的int形变量可以作为数组长度参数:
1
2
int keyVals[] = {1,3,7,9,11,22,35};
int mappedVals[arraySize(keyVals)];//此处使用std::array更佳


Function Arguments

 
数组名并非是C++中唯一会退化成指针的东西,函数名亦然,因此我们针对数组的所有讨论都可以应用于函数,举例而言:

1
2
3
4
5
6
7
8
9
10
void someFunc(int,double);//type is void(int,double)

template<typename T>
void f1(T param);/pass-by-value

template<typename T>
void f2(T& param);//pass-by-reference

f1(someFunc);//ParamType's type is void(*)(int,double)
f2(someFunc);//ParamType's type is void(&)(int,double)

好像在实际应用中没有什么差别,但知道总比不知道要好。


总结

  1. 在普通模板参数推衍过程中,reference-ness会被忽略。
  2. 对于universal reference,lvalue作为实参传入时会保留reference-ness。
  3. 当以pass-by-value形式类型推衍时,const-ness与volatile-ness会被忽略。
  4. 在模板类型推衍过程中,数组名与函数名会退化为指针,除非形参以引用形式得到了初始化。

类型推衍

Posted on 2018-06-19 | In Effective Modern C++

章节前言

 
C++98对于类型推衍有单独的一套规则,它们被应用于函数模板的使用过程中。C++11在此基础之上做了些许修改并增加了2条额外推衍规则,即auto与decltype。随后的C++14则扩展了auto与decltype的使用范围。类型推衍使开发者们得以免于反复输入那些直观明了的类型名称,并且大大增加了C++程序的可修改性,因为在存在类型推衍的情况下,一旦源码中某个变量的类型被更改,其他位置与其相关的类型也会自发进行更改。但是类型推衍并非完美无缺,它的存在使得代码更加难以理解,原因在于编译器所作出的类型推衍可能并非像你想象的那样显然。

如果你不能清楚认知类型推衍规则,这一悲惨的事实将导致你无法高效使用Modern C++。以上并非危言耸听,类型推衍在很多地方发挥着作用,例如函数模板、auto、decltype表达式,以及C++14中所出现的decltype(auto) construct。

本章将对C++开发者所必须了解的类型推衍规则作出介绍,其主要内容为:

  1. 模板类型推衍的工作机制
  2. auto的工作机制
  3. decltype的工作机制

在此基础之上,本章还将介绍如何令编译器将其类型推衍结果可视化,从而保证你可以放心大胆地认为它按照你的要求完成了类型推衍。

站在对象模型的顶端——弹性

Posted on 2018-05-31 | In Inside the C++ object model

前言

 
传统C++对象模型提供高效的执行期支持,然而在某些领域,例如动态共享函数库、共享内存以及分布式对象方面,该对象模型仍然缺乏弹性。


动态共享函数库

 
在理想情况下,一个动态链接的shared library应该如同“突然来访”。也就是说,当应用程序下一次再执行时,会透明化地取用新的library版本,新library的出现不应该对旧的应用程序产生侵略性,应用程序不应当需要重新编译。然而,当前的C++对象模型无法做到这一点。


共享内存

 
当一个shared library被加载,它在内存中的位置由runtime linker决定,一般而言与执行中的process无关。然而,在C++对象模型中,当一个动态的shared library支持一个class object,其中含有置于shared memory中的virtual functions时,上述说法失效。

站在对象模型的顶端——RTTI

Posted on 2018-05-31 | In Inside the C++ object model

(本节内容可参见More Effective C++ RTTI一篇)

站在对象模型的顶端——异常处理

Posted on 2018-05-31 | In Inside the C++ object model

前言

 
想要支持exception handling,编译器的主要任务是找出catch子句,以处理被丢出来的exception。这需要追踪程序堆栈中的每一个函数的当前作用区域。同时,编译器必须提供某种查询exception objects的方法,以了解其实际类型(这直接引发了RTTI的出现)。最后,还需要某种机制用以管理被丢出的object。一般而言,exception handling机制需要与编译器所产生的数据结构以及执行期的一个exception library紧密合作。在程序大小和执行速度之间,编译期必须有所抉择:

  • 维持执行速度
    编译器可以在编译时期建立起用于支持的数据结构,这会使程序代码膨胀。
  • 维持程序大小
    编译器在执行期建立用于支持的数据结构,这会影响程序的执行效率。

Exception Handling快速检阅

 
C++的exception handling由三个主要的词汇组件构成:

  1. 一个throw子句。它在程序某处发出一个exception,被丢出的exception可以是内建类型,也可以是用户自定义类型。
  2. 一个或多个catch子句,每一个catch子句都是一个exception handler。其含义在于表明某子句处理某种类型的exception,并且在封闭区段中提供实际处理程序。
  3. 一个try区段,这个区段中的内容可能会引发catch子句起作用。

当一个exception被丢出时,控制权会从函数调用中被释放出来,并寻找一个吻合的catch子句。如果不存在吻合者,那么默认的处理例程terminate()会被调用。当控制权被放弃后,堆栈中的每一个函数调用也就被推离,这个程序称为unwinding the stack。在每一个函数被推离堆栈之前,函数的local class objects的destructor会被调用。

问题实例

 
Exception handling会给函数带来一定的影响,考虑下述函数:

1
2
3
4
5
6
7
8
Point* mumble(){
Point *pt1,*pt2;
pt1=foo();
if(!pt1) return nullptr;
Point p;
pt2=foo();
if(!pt2) return pt1;
}

如果有一个exception在第一次调用foo()时被抛出,那么这个mumble()函数会被推出程序堆栈。由于本函数不存在任何try语句,因此不会有任何catch子句需要和他结合,不过幸运地是这里也没有任何local class object需要析构。如果有一个exception在第二次调用foo()时被抛出,exception handling机制就必须在从程序堆栈中unwinding这个函数之前,先调用p的destructor。

在Exception handling之下,L2~L4以及L5~L7被视为两个语意不同的区域,因为当exception被抛出时,这两个区域存在不同的执行期语意。
(接下来作者探讨了exception对资源的影响,并且引出了RAII的概念)


对Exception Handling的支持

 
当一个exception发生时,编译系统必须完成以下事情:

  1. 检验发生throw操作的函数
  2. 判断throw操作是否发生在try语句中
  3. 如果在try中,则进行catch子句匹配
  4. 若发生匹配,则将流程控制转交catch子句
  5. 如果3、4并没有符合,
    那么系统必须摧毁所有active local object,从堆栈中unwind当前函数,并且进行到程序堆栈中的下一个函数,重复2-5操作。

判断throw操作是否在try中

之前曾经介绍过,一个函数可以被划分为很多区域:

  • try区段以外的区域,而且没有active local objects
  • try区段以外的区域,但存在active local objects需要析构
  • try区段以内的区域

编译器必须标示上述区域的一个有效策略是构造 program counter-range表格。

program counter(Intel CPU中为EIP缓存器)内含一个即将执行的程序指令,为了在一个内含try区段的函数中标示出某个区域,可以把program counter的起始值和结束值存储在一个表格中。当throw操作发生时,当前的program counter值将会与表格内部存储值进行比对,从而确定当前是否在try区段中,从而进行下一步操作。

catch子句匹配

对于每一个被抛出的exception,编译器必须产生一个类型描述器,对exception的类型进行编码。如果那是一个derived type,则编码内容必须涵盖其所有base class的类型信息。只编入public base class的信息是不够的,因为这个exception可能被一个member function捕获,而在一个member function的scope中,derived class与nonpublic base class之间允许相互转换。

由于exception handling发生在执行期,因此我们必然需要RTTI。编译器还必须为每一个catch子句产生一个类型描述器,它们会试图去与exception的类型描述器匹配,直到吻合,或者调用terminate()。

抛出实际对象带来的后果

当一个exception被丢出时,exception object会被产生出来并置于相同形式的exception数据堆栈中,从throw端传播至catch子句的是exception object的地址、类型描述器等等。

考虑一个catch子句如下:

1
2
3
4
catch(exPoint p){
//do sth
throw;
}

并且有一个exception object,其类型为exVertex,派生自exPoint。当前p会发生什么?

  • p将以exception object作为初值。
    如果存在copy constructor或destructor,将会发生一次拷贝构造
  • 发生slice
    由于p是一个object而非reference,所以在copy过程中发生了slice off.p的vtbl将被设为exPoint的vtbl,vptr不会发生改变。

如果我们再次抛出p,此时我们将建立一个临时对象,并且exVertex的性质将完全丢失。(本小节内容可参见More effective C++ 关于异常处理的几篇)

<i class="fa fa-angle-left"></i>1…91011…27<i class="fa fa-angle-right"></i>

xander.liu

266 posts
11 categories
36 tags
RSS
GitHub E-Mail
© 2024 xander.liu
Powered by Hexo
|
Theme — NexT.Pisces v5.1.4