Xander's Wiki


  • Home

  • Tags

  • Categories

  • Archives

  • Search

28.尽量避免转型操作

Posted on 2018-04-14 | In Effective C++

(本节内容联系More Effective C++ 5 效果更佳)

前言

 
c++规则的设计目标之一就是:保证类型错误绝不可能发生。理论上而言,如果你的程序能很干净地通过编译,就说明了它并不企图在任何对象身上执行任何不安全、无意义、荒谬的操作。
但转型(cast)破坏了类型系统。在Java,C#,或C中,转型操作必要且较为安全(当然,安全是相对的),C++的转型则充斥着风险。


转型语法

旧式转型

1
2
(T)expression;//将expression转为T
T(expression);//将expression转为T

两者并无差别,无非是小括号的位置不同。

C++转型

C++提供了四种新式转型操作符:

  • const_cast(expression)
  • dynamic_cast(expression)
  • reinterpret_cast(expression)
  • static_cast(expression)

C++转型操作符介绍

  • const_cast:能够将对象的const属性去除,也只有它拥有该能力
  • dynamic_cast:主要用来执行“安全向下转型”,也就是用来决定某对象是否归属继承体系的某个类型。它是唯一一个无法由旧式语句转换的新类型转换,也是唯一一个可能耗费大量运行成本的转型操作。
  • reinterpret_cast:意图执行低级转型,实际动作可能取决于编译器,这一特性也就意味着不可移植。比如将一个pointer to int转为int,该转型操作在低级代码以外相当少见。
  • static_cast:强迫隐式转换,比如把non-const转为const,int转为double。它也可以作上述转换的反向转换,例如把pointer-to-base转为pointer-to-derived,或者把void*指针转为typed指针。但是没法把const转为non-const。

新旧转型的优劣以及使用时机

新式转型的优点

尽管旧式转型依然合法,但新式转型更受青睐。原因在于:

  1. 易于辨识
  2. 转型动作明确

何时采用旧式转型

一般来说,仅在需要调用一个explicit构造函数将一个对象传递给一个函数时使用旧式转型:

1
2
3
4
5
6
7
8
class Widget{
public:
explicit Widget(int size);
...
};
void doSth(const Widget& w);
doSth(Widget(15));
doSth(static_cast<Widget>(15));

然而这种风格并不像是转型,更像是一种构造。但必须明确,这里其实执行的类型转换操作。


类型转换的意义

 
有人认为转型只不过告诉编译器把某种类型视为另一种类型,其他什么都没做,实际上这是不可能的
。将int转为double的时候编译器肯定生成了一些代码,因为int的底层描述与double的底层描述并不相同。再比如说:

1
2
3
4
class Base{...};
class Derived:public Base{...};
Derived d;
Base* pb = &d;//暗中将Derived*转为了Base*

我们建立了一个base指针指向derived class对象,但有时上述的两个指针值并不相同,这时会有一个偏移量在运行期间被施行于derived*指针,以取得正确的base*值。

上述案例表明,单一对象可能拥有多个地址(这在C,Java,C#中是不可能发生的)。如果在C++中使用多重继承,单一对象必然拥有多个地址,并且在单一继承下也可能存在多个地址。我们应该避免做出“对象在c++中如何布局”的假设,更不应该以某种假设为基础执行任何转型操作。


类型转换的误区

 
类型转换可能会导致我们易写出一些似是而非的代码,(它们在其他语言中可能是正确的)。

问题实例

许多应用框架都要求derived classes内的virtual函数第一件事就是调用base class的对应函数。下面给出一个具体实例:

1
2
3
4
5
6
7
8
9
10
11
class Window{
public:
virtual void onResize() {...}
};
class SpecialWindows:public Window{
public:
virtual void onResize(){//要求首先调用base class的对应函数
static_cast<Window>(*this).onResize();//error!
...//执行专有操作
}
}

我们确实成功地将它转为了Window对象,也确实调用了Window::onResize。但值得注意的是,调用的并不是当前对象上的函数,而是稍早转型动作建立的一个“*this对象之base class成分”的暂时副本身上的onResize(),也就是说当前对象内部的数据并没有改变,改变的是一个副本。(这个原理很简单,就是转型操作导致了一个临时Window对象被生成,我们在该临时对象身上调用了onResize,伴随着它的析构,这个操作就像没做一样,(*this)的bc成分根本没有改变)

解决方案

解决方案就是直接调用,不要使用转型操作:

1
2
3
4
5
6
7
class SpecialWindows:public Window{
public:
virtual void onResize(){
Window::onResize();
...//执行专有操作
}
}


dynamic_cast探究

 
首先需要强调的是,dynamic_cast执行速度相当缓慢,如果继承层次很深或者存在多继承的情况下使用dynamic_cast,可能对性能的冲击极大,请谨慎使用!

dynamic_cast功能

之所以需要使用dynamic_cast,主要是因为我们需要在一个derived class对象身上执行derived class专属操作,但偏偏我们目前只有一个指向base的pointer或reference.

如何避免使用dynamic_cast

使用容器,并指定其存储的元素类型为指向derived class对象的指针

举例而言,假设在之前的体系中,SpecialWindow有一个特有操作:闪烁

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Window{...};
class SpecialWindows:public Window{
public:
void blink();
...
};
//需要闪烁所有可以闪烁的窗口
typedef vector<shared_ptr<Window> > VPW;
VPW winPtrs;
...
//使用dynamic_cast
for(auto it=winPtrs.begin();it!=winPtrs.end();++it){
//赋值,检测其返回值不为nullptr
if(SpecialWindow *psw = dynamic_cast<SpecialWindows*>(iter->get()))
psw->blink();
}

应该改为在容器内存在指向dc的smart_ptr:
1
2
3
4
5
6
typedef vector<shared_ptr<SpecialWindow> > VPSW;
VPSW winPtrs;
...
for(auto it=winPtrs.begin();it!=winPtrs.end();++it){
(*iter)->blink();
}

不过,这种做法无法在同一个容器内存储指向所有派生类的指针。

通过base接口处理所有可能的派生类

具体来说,就是在基类中声明所有派生类所具备的特殊函数,然后提供一份空白实现。(这种方法十分恶劣,违背了面向对象准则)

dynamic_cast禁区

绝对必须避免的是连锁式使用dynamic_cast,其运行效率极低且缺乏维护性。这种程序总应该被某种基于virtual函数调用的方法取而代之。


总结

 
优秀的c++代码很少使用转型,但完全摆脱它们也不切实际。我们要做的只能是尽可能隔绝转型动作,把他们隐藏在某个函数内。

29.需要依次录入字符时考虑使用istreambuf_iterator

Posted on 2018-04-14 | In Effective STL

问题实例

 
假设我们需要拷贝一个文本文件到一个字符串:

1
2
3
4
//该程序仅作示例,实际编译效果与设想不符,原因见Effective STL 6)
ifstream inputFile("interestingData.txt");
string fileData((istream_iterator<char>(inputFile)),
istream_iterator<char>());

实际上这并没有完全copy文本,因为istream_iterator使用operator>>,该操作符在默认情况下忽略空格。
如果你不想所有空格都莫名消失,那你应该覆盖默认情况:
1
2
3
ifstream inputFile("interestingData.txt");
inputFile.unset(ios::skipws); // 关闭inputFile的忽略空格标志
string fileData((istream_iterator<char>(inputFile)), istream_iterator<char>());

然而你会发现这种拷贝速度很慢,因为operator>>采取格式化输入,它需要建立sentry对象(为每个operator>>调用进行建立和清除活动的特殊的iostream对象),必须检查流标志位,以及是否需要抛出异常等等操作,这些操固然很重要,但当我们需要的只是从输入流中抓取下一个字符时,这一切都是不必要的。


解决方案

 
istreambuf_iterator的特性是直接进入输入流缓冲区读取字符,使用方法如下:

1
2
3
4
//再次重申这个没法正常运行,只是示例
ifstream inputFile("interestingData.txt");
string fileData((istreambuf_iterator<char>(inputFile)),
istreambuf_iterator<char>());

istreambuf_iterator也无需考虑空格的因素,因为空格不会被输入流摒弃。
同样的,逐个输出字符时也应该使用ostreambuf_iterator.

27.尽可能延后变量定义式的出现时间

Posted on 2018-04-14 | In Effective C++

前言

 
只要你定义了一个需要构造和析构的成员变量,那么当程序的控制流转移到该变量定义式时,你就必须承担构造成本。同样地,当控制流离开其作用域时,你必须承担析构成本。即使这个变量从未被使用。


问题实例

 
你或许认为没人会定义一个不使用的变量。请看下述程序:

1
2
3
4
5
6
7
8
9
10
string encryptPassword(const string& password){
//加密文本串,如果串过短则抛出异常
using namespace std;
string encrypted;
if(password.length()<MinLength){
throw logic_error("Password is too short");
}
...
return encrypted;
}

在此例中,对象encrypted可能并未使用,所以最好延后其定义式,直到确实需要使用该对象为止:
1
2
3
4
5
6
7
8
9
10
string encryptPassword(const string& password){
//加密文本串,如果串过短则抛出异常
using namespace std;
if(password.length()<MinLength){
throw logic_error("Password is too short");
}
string encrypted;//肯定能被使用
...
return encrypted;
}

但上述代码仍然存在提升的空间,请注意encrypted对象虽然获得了定义但并没有值(执行了默认初始化),在Effective C++ 5中我们了解到“通过default构造函数构造然后再赋值”的行为比“直接指定初值”的效率要低,所以,正确的写法应当如下:
1
2
3
4
5
6
string encryptPassword(const string& password){
...//检查长度
string encrypted(password);//直接copy构造
encrypt(encrypted);//加密函数
return encrypted;
}

这才是延后变量定义式的出现时间的真谛:可能地延后变量的定义直到可以直接用某个具体值初始化它为止。


定义式与循环

 
如果变量仅在循环内部使用,那么是应该把它定义在循环内还是循环外?也就是说,写法A与写法B,哪一种的效率更高?

1
2
3
4
5
//写法A 循环外定义,循环内赋值
Widget w;
for(int i=0;i<n;++i){
w=..//赋值
}

1
2
3
4
5
//写法B 循环内定义
Widget w;
for(int i=0;i<n;++i){
w(...)//copy初始化
}

两种写法的成本如下:

  • A:1次构造,1次析构,n次赋值
  • B:n次构造,n次析构

那么显然地,如果赋值成本较低,那应当使用A。但A造成了w的作用域不仅仅在循环内,这可能会降低代码的可读性与易维护性。除非你必须要对效率进行优化,否则写法B相较于A更佳。


总结

 
应当尽可能延后变量定义式出现的时间,这不仅有利于效率,也提高了程序的可读性。

28.使用base将reverse_iterator转为iterator

Posted on 2018-04-14 | In Effective STL

前言

</br>
我们可以通过reverse_iterator的base函数将其转为iterator,只是其结果可能和我们预想的存在出入。


问题实例

</br>
考虑如下程序:

1
2
3
4
5
6
vector<int> v(5);
for(int i = 0;i < 5; ++ i) {
v.push_back(i);
}
vector<int>::reverse_iterator ri = find(v.rbegin(), v.rend(), 3);//ri指向3
vector<int>::iterator i(ri.base());

转换关系图

image_1cb13q24e2eq1tpbfst11rl12hjp.png-14.2kB
这张图清楚地展示了偏移量,但对我们解决实际问题毫无帮助。众所周知,insert与erase只接受iterator形式的参数,我们该如何通过base()来正确地完成这二者的操作呢?


base与insert、erase

insert

我们假定需要在ri指出的位置插入一个99。
因为ri遍历的顺序是自右向左,而且insert会把新元素插入到ri位置,并且把原先ri位置的元素依次移动到符合遍历顺序的下一个位置,综上,插入后应该是这样:image_1cb14faq517l4kkhivjap118oi16.png-3.8kB
显然,99正对应着base的位置,事实上有结论如下:
如果要在一个reverse指出的位置上插入新元素,直接使用base即可,对于insert而言,ri.base()就是ri对应的iterator.

erase

如果要删除3肯定不能使用base了,因为base指向4.
但也不是没有解决办法,ri的base指向4,(++ri).base()不就指向3了吗,所以说核心在于先自增:

1
v.erase((++ri).base());

26.自定义一个不抛异常的swap函数

Posted on 2018-04-14 | In Effective C++

前言

</br>
swap原先只是STL的一部分,而后成为异常安全性编程的脊柱,以及用来处理自赋值的常见机制。(详见Effective C++ 12)。swap实现的复杂程度极高,本节主要探讨其复杂程度以及应对策略。


swap的具体实现

STL的swap实现

1
2
3
4
5
6
7
8
namespace std{
template<typename T>
void swap(T& a,T& b){
T temp(a);
a=b;
b=temp;
}
}

只要类型T支持copying操作,该swap就一定能正常运行。
该行为必然是正确的,只是对于某些类型而言,其copying行为是不必要的,降低了程序运行的性能。

问题实例

对于”内部仅含指针,指针指向具体数据”的class而言,STL版本的swap函数过于低效。这种类型的常见表现形式是所谓的“pimpl”(pointer to implementation)(详见Effective C++ 32)。具体如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class WidgetImpl{//具体实现类
public:
...
private:
int a,b,c;
vector<string> vs;//内部存有大量数据,复制占用时间极大
}
class Widget{
public:
Widget(const Widget& rhs);
Widget& operator=(const Widget& rhs){//复制具体实现(其实这里也可以直接复制指针,引用计数)
*pImpl=*(rhs.pImpl);
}
private:
WidgetImpl* pImpl;
}

显然,如果我们需要swap两个widget对象,只需要交换两个指针值。如果使用STL::swap,它不仅复制3个widget,还复制了3个widgetImpl.何以说是三个?

  1. temp(a)
    此时调用拷贝构造,复制了一个Widget,在复制Widget时,其拷贝构造必然构造了一个WidgetImpl
  2. a=b
    调用Widget::operator=,复制了一个WidgetImpl
  3. b=temp
    同上

解决思路

我们希望告诉std::swap,置换widget只需要置换内部指针。无疑我们需要将std::swap针对Widget特例化,以下是无法通过编译的基本构思:

1
2
3
4
5
6
namespace std{
template<>
void swap<Widget>(Widget&a,Widget&b){
swap(a.Impl,b.Impl);
}
}

在一般情况下不允许改变std命名空间的任何东西,但是这里只是添加了某个特例化版本,属于安全行为。(Effective STL 42 也这样做了)
之所以无法编译是因为我们试图去访问private成员。使用friend函数当然是可耻的:封装性被破坏。

解决方案

定义一个member function做真正的swap工作,然后再令STL的swap函数调用它:

1
2
3
4
5
6
7
8
9
10
11
12
13
class Widget{
public:
void swap(Widget& other){
using std::swap;//必须声明
swap(pImpl.other.pImpl);
}
}
namesapce std{
template<>
void swap<Widget>(Widget& a,Widget& b){
a.swap(b);
}
}

这种写法不仅能够通过编译,并且还与stl容器兼容,因为stl容器也提供了public swap成员函数与std::swap特例化版本。

template下的swap实现

无法完成function template偏特化

假定现在Widget与WidgetImpl都是template class:

1
2
3
4
template<typenmae T>
class WidgetImpl{...};
template<typename T>
class Widget{...};

此时在Widget::swap照旧,但试图特化std::swap时却遇到了麻烦:
1
2
3
4
5
6
namespace std{//无法编译
template<typenmae T>
void swap<Widget<T> >(Widget<T>& a,Widget<T>& b){
a.swap(b);
}
}

不能编译的理由是:我们企图偏特化(partially specialize)一个function template(std::swap),但C++只允许对class template执行偏特化。(所谓偏特化就是针对某个部分执行特例化,但特例化之后得到的却仍然是一个模板而非实例,在本例中,swap本身是一个function template,我们试图针对其参数为Widget<T>时特例化,但特例化得到的仍然是一个function template,而非像上文那样的实例函数。关于偏特化的介绍,详见C++ Primer P628)

std命名空间禁止添加新的重载template

如果你需要偏特化一个template function,那么常规做法为其添加一个重载版本:

1
2
3
4
5
6
namespace std{//这也是不合法的
template<typename T>
void swap(Widget<T>& a,Widget<T>& b){
a.swap(b);
}
}

对于std命名空间,客户可以全特化其中的template,但不允许添加任何新的template进入,上述代码属于新的swap。

声明non-member swap

我们将定义一个non-member swap函数,由它调用member swap,但不再将non-member swap声明为std::swap的特例化,为了简化说明,我们将一切与Widget相关的机能都置入WidgetStuff命名空间:

1
2
3
4
5
6
7
8
namespace WidgetStuff{
template<typename T>
class Widget {...}
template<typenmae T>
void swap(Widget<T>& a,Widget<T> &b){
a.swap(b);
}
}

如此一来,根据C++的名称查找法则,每当试图swap Widgte对象时,都会第一时间找到我们所定义的non-member版本,这并非令客户失去了定制操作的能力。只要加上using std::swap,那么他们的swap又会回归STL版本,这也是我们的member swap使用using的原因。


swap与异常

</br>
member swap禁止抛出异常,但非成员版允许抛出异常,因为默认swap以copying函数为基础,而copying行为允许抛出异常。
自定义swap意味着高效交换,并且不抛出异常。这两个特性其实彼此相关:高效交换的基础是基于内置类型的操作,而内置类型的操作绝不会抛出异常。


总结

  1. 当std::swap对class效率不高时,提供一个swap成员函数并且确定其noexcept
  2. 如果你提供了member swap函数,也应该提供一个non-member来调用前者。对于classes(并非templates),也请特例化std::swap
  3. 调用swap时应该先声明使用的是哪个命名空间内的swap
  4. std命名空间允许全特例化,但禁止在其内部加入一些对std而言全新的东西

27.const_iterator到iterator的转换(distance、advance)

Posted on 2018-04-14 | In Effective STL

前言

</br>
正如上一节所说,insert与erase只支持iterator或能隐式转为iterator的迭代器。如果我们只有一个const_iterator,那怎么办?有人会试图去使用类型转换:

1
2
3
4
5
typedef deque<int> IntDeque; 
typedef IntDeque::iterator Iter;
typedef IntDeque::const_iterator ConstIter;
ConstIter ci; // ci是const_iterator
Iter i(const_cast<Iter>(ci)); //error

不能类型转换原因是很简单的,const_iterator与iterator是完全不同的类,就像string与vector<int>没有任何关联一样。


解决思路

1
2
3
4
5
6
7
typedef deque<int> IntDeque;
typedef IntDeque::iterator Iter;
typedef IntDeque::const_iterator ConstIter;
IntDeque d;
ConstIter ci;
Iter i(d.begin());
advance(i, distance(i, ci));

advance与distance都在头文件<iterator>中。
distance返回两个指向同一个容器的iterator之间的距离;advance则用于将一个iterator移动指定的距离。
如果i和ci指向同一个容器,那么表达式advance(i, distance(i,ci))会将i移动到与ci相同的位置上。
思路很美好,但实际上这段程序不能编译通过。


解决方案

无法编译的原因

distance的定义式如下:

1
2
3
template<typename InputIterator>
typename iterator_traits<InputIterator>::difference_type
distance(InputIterator first, InputIterator last);

我们可以清楚地看到distance要求两个参数必须一致,最起码能完成隐式转换,而实际上const_iterator无法转换。

显式指定函数模板参数

通过显式地指明distance调用的模板参数类型,那么编译器则不再从参数推断函数模板参数:

1
advance(i, distance<ConstIter>(i, ci));

效率

对于随机迭代器(vector,string,deque)消耗常数时间,双向迭代器(所有其他容器)需要线性的时间。

25.若所有参数均需类型转换,请为此采用non-member函数

Posted on 2018-04-13 | In Effective C++

前言

</br>
前文曾说过,令classes支持隐式转换通常会带来风险,但这也有例外,比如当你建立的是一个数值类型。


问题实例

</br>
假设我们建立了一个有理数类,它允许整数隐式转换为有理数:

1
2
3
4
5
6
7
class Rational{
public:
Rational(int numerator=0,int denominator=1);//刻意不写为explict
int numerator() const;//成员的访问函数
int denominator() const;
...
}

接下来需要纠结的就是运算符到底是写为member还是non-member呢?


问题处理

</br>
首先考虑声明为member函数的情况:

1
const Rational operator* (const Rational& rhs) const;

那么很自然地,两个Rational对象可以很自由地相乘。但你会发现这个函数没法做到混合类型计算,比如说,一个int乘以一个ratioanl对象:
1
2
res = oneHalf * 2;//正确,因为执行了隐式转换
res = 2 * oneHalf;//错误

不能编译的原因很简单,int并没有成员函数operator*。解决的方法也很简单,把operator*声明为non-member函数即可:
1
const Rational operator* (const Rational& lhs,const Rational& rhs);

如此一来,混合类型计算的问题就得到了完美解决,哪怕两个参数都不是Rational,但只要它们存在直接变为Rational对象的隐式转换即可顺利编译与运行。


friend函数

</br>
那运算符是否需要成为friend函数呢?答案也是否定的。friend函数可以直接访问内部数据,这大大降低了封装性。当然friend函数也有优点,但我们应该牢记:不能因为函数不该成为member,就自动让它成为friend.


总结

</br>
本节叙述的内容仅在OO领域内生效,当我们进入template领域并令Rational成为一个class template后,又会出现新的争议、解法、以及设计思路。
但至少在OO领域内我们应当牢记:如果某个函数的所有参数都可能需要进行类型转换,那么这个函数必须是个non-member.

26.尽可能以iterator代替其const或reverse版本

Posted on 2018-04-13 | In Effective STL

四种迭代器介绍

</br>
对于container<T>而言,iterator的作用相当于T*,而const_iterator则类似于const T*.
reverse_iterator与const_reverse_iterator同样相当于对应的T*和const T*,和上两种所不同的是,它们的遍历次序是从尾到头。

尽可能不使用其余三种的原因

成员函数形参要求

以vector举例,其insert与erase的样式如下:

1
2
3
iterator insert(iterator position, const T& x);
iterator erase(iterator position);
iterator erase(iterator rangeBegin, iterator rangeEnd);

需要注意的是:这两个函数只接受iterator类型的参数,而不是const_iterator、reverse_iterator或const_reverse_iterator。

四种迭代器之间的相互转换

image_1cav8o342170n1eeha5v1rpge239.png-20.5kB
箭头表明了它们可以执行隐式转换,而且reverse可以调用base成员函数转为iterator(详见Effective STL 28)值得注意的是const迭代器无法转为普通版本,也就是说,当你需要指出插入或者删除位置的元素时,const迭代器几乎没用。

使用iterator的优越性:

  1. 兼容insert与erase
  2. const版本无法隐式转换普通版本,就算能转效率也不是很高
  3. reverse版本转换来的iterator需要相应的调整

何时应该选用reverse呢?比如说你真的需要从后向前遍历的时候。

24.以non-member non-friend函数替换member函数

Posted on 2018-04-13 | In Effective C++

问题实例

</br>
假定现有一个class网页浏览器,其提供了一些清除cache,历史记录,或者cookies的成员函数:

1
2
3
4
5
6
7
8
class WebBrowser{
public:
...
void clearCache();
void clearHistory();
void removeCookies();
...
};

可能有用户会试图一起执行这些操作,因此WebBrowser也提供这样一个member函数:
1
2
3
4
5
6
class WebBrowser{
public:
...
void clearEverything();//调用三个成员函数
...
};

当然,这个功能也可以由一个non-member函数完成:
1
2
3
4
5
void clearBrowser(WebBrowser& wb){
wb.clearCache();
wb.clearHistory();
wb.removeCookies();
}

那么二者孰优孰劣?


封装性

</br>
根据面向对象原则,数据及操作数据的函数应当尽量被捆绑到一起,所以说member函数似乎比non-member函数更能体现封装性。但是事实并非如此,member函数的封装性较低。另外,non-member函数可允许对WebBrowser相关机能有较大的packaging flexibility,而这导致了较低的编译相依度,增加了WebBrowser的可延伸性。

封装的意义

如果某些东西被封装,这意味着它不再对外可见,因此我们也拥有了较大的弹性去改变它。因此,愈多东西被封装,我们能够改变它们的能力也就越大。封装让我们改变事物但只影响有限客户。

封装性的度量

我们可以粗略地认为,有越多的函数可以访问某数据,该数据的封装性也就越低。
上节说过,成员变量应该是private,否则这意味着有无限的函数可以访问它。那么显然的,如果一个non-member和一个member实现相同的功能,那我们应该优先使用non-member函数,因为它可以访问的数据很相对较少,也就是拥有更好的封装性。


non-member non_friend函数

</br>
以上论述需要注意两点:

  1. 我们讨论的其实是non-member non-friend函数,因为friend函数与member函数具备相同的封装冲击性。所以从封装的角度而言,这里并非是在member函数与non-member函数之间选择,而是在member函数与non-member non-friend之间选择。
  2. 因为在意封装性从而让函数成为class的non-member,并不意味着它不可以是另一个class的member。
    比如令令成为某个工具类的static member函数,只要它不是WebBrowser的一部分(或成为其friend),就不会影响WebBrowser的private成员封装性。

实现策略

在C++中,比较自然的方法是令clearBrowser成为一个non-member函数并与WebBrowser处在同一个namespace内:

1
2
3
4
namespace WebBrowserStuff{
class WebBrowser {...};
void clearBrowser(WebBrowser& wb);
}

namespace的一大优点在于它可以跨越多个源码文件。我们为了实现编译分离,可以在不同的头文件内声明各类函数,然后把他们放在同一个命名空间内。如果我们需要更多的non-member函数,那么我们只需要往namespace里面添加就好了。这种方式允许客户只对他们使用的那一小部分形成编译相依。

24.针对性地使用map::operator[]与map::insert

Posted on 2018-04-13 | In Effective STL

问题实例

</br>
假设有一个Widget类:

1
2
3
4
5
6
7
class Widget {
public:
Widget();
Widget(double weight);
Widget& operator=(double weight);
...
}

我们试图建立起int、widget之间的映射关联,那么使用使用map是再恰当不过的:
1
2
3
map<int, Widget> m;
m[1] = 1.50;//一一添加映射关系
m[2] = 3.67;

然而这种写法对性能的冲击极大。


map::operator[]与map::insert

map::operator[]的具体实现

map的operator[]与别的容器的operator[]颇有不同,它所执行的功能是“更新或添加”,具体来说,

1
m[k] = v;

上述表达式所执行的操作依次为:

  1. 检查键k是否在map中,如果没有,则insertpair<const k,v>
  2. 有,则更新k所对应的value

operator[]默认返回一个value的引用,这在更新操作中再正常不过,但对于insert操作,它会使用value的默认构造函数构造一个,然后返回这个新建立对象的引用。具体来说,

1
m[1] = 1.50;//m中不存在k==1

等价于
1
2
3
typedef map<int, Widget> IntWidgetMap;
pair<IntWidgetMap::iterator, bool> result = m.insert(IntWidgetMap::value_type(1, Widget()));
result.first->second = 1.50;

用insert直接取代operator[]中的插入操作

显然,上述代码在map中插入了一个pair,其first是我们指定的key,second则是默认初始化的widget,最后再执行了赋值操作。
以value初始化widget自然会比上述代码更加高效,所以,我们应该直接使用insert代替operator[]:

1
m.insert(IntWidgetMap::value_type(1, 1.50));

这种写法一共节约了3次函数调用:

  • 一次建立widget临时对象
  • 一次销毁widget临时对象
  • 一次widget赋值操作

两全其美的方法

</br>
出于对更高效的渴望,我们能否扬长避短,实现如下的功能?

1
2
3
//如果k在m中,调用operator[],否则执行insert
//返回被指向pair的迭代器
iterator affectedPair = efficientAddOrUpdate(m, k, v);

实现该函数实际上并不困难:
1
2
3
4
5
6
7
8
9
10
11
12
13
template<typename MapType,typename KeyArgType, typename ValueArgtype>
typename MapType::iterator //利用typename强调此处是一个类型而非成员
efficientAddOrUpdate(MapType& m,const KeyArgType& k,const ValueArgtype& v){
typename MapType::iterator Ib = m.lower_bound(k);//找出合理插入位置
if(Ib != m.end() && !(m.key_comp()(k, Ib->first))) {//关联容器判同
Ib->second = v; //更新
return Ib;
}
else{
typedef typename MapType::value_type MVT;
return m.insert(Ib, m::value_type(k, v));//常数时间完成插入
}
}

在上述实例中,keytype与valuetype不必是存储在map里的类型,它们只需要能够完成隐式转换即可。


总结

</br>
本节的重点并不在于如何去实现AddOrUpdate这样的函数,而在于强调operator[]与insert在不同情况下效率的不同,如果我们能明确程序行为,那我们应当谨慎地选用它们中的一个。

<i class="fa fa-angle-left"></i>1…212223…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