本文整理了C++的一些运行时的惯用法。

pimpl惯用法

对于一个类型T来说,如果内部使用了其他的类型U:

特别的,将T需要的所有数据都封装起来,放到U中,而T的数据成员仅包含U*,接口信息定义在T的头文件中,而所有的实现,包括U的实现都放到实现文件中,这就是经典的pimpl惯用法。

例子:

// 头文件
#pragma once
 
#include <boost/shared_ptr.hpp>
 
class MyClass
{
public:
    MyClass();
    void foo();
 
private:
    class MyClassImpl;       // private下私有的内部类 前置声明
    boost::shared_ptr<MyClassImpl> m_pImpl;   // impl的指针
};
// 实现文件
#include "MyClass.h"
 
#include <iostream>
 
// implement of impl
class MyClass::MyClassImpl
{
public:
    // 实现需要的一些函数,最终用于实现MyClass
    void foo()
    {
       std::cout << "impl foo" << std::endl;
    }
};
 
// implement class using impl
MyClass::MyClass() :
    m_pImpl( new MyClassImpl )
{
}
 
void MyClass::foo()
{
    if ( m_pImpl )
    {
       m_pImpl->foo();
    }
}
// main函数
#include "MyClass.h"
 
int main()
{
    MyClass cls;
    cls.foo();
 
    return 0;
}

输出:
impl foo

pimpl惯用法的一些好处:

  1. 将实现与接口声明完全分离,不依赖于任何类型(因为只使用了指针),接口非常清晰。
  2. 由于impl代理类的定义也是在实现文件中,这样如果要修改实现只要修改该实现文件,并且编译的时候只会编译该实现文件。

动态数组

很多C++的程序中存在着大量的由类似 char * p = new char[Length]; 产生的动态数组。带来的是各种异常返回情况都要显式的去匹配调用delete []p; 万一有遗漏,很可能产生内存泄漏,而且不小心容易漏写了[]。

另外,如果某函数分配的内存需要被其他函数使用,这样没问题,只要把地址或者智能数组传递出去,但如要用其他函数来释放内存,那就是函数流程设计有问题。

总之,为了消除为匹配释放内存块而产生的麻烦,我们尽量不要去显示的使用new T [] & delete []p对来管理内存块,而是使用能自动管理内存块生命期的类。

有两个比较好的选择:

scope_array由于是独占的,不能拷贝,非常适合在函数中一次性使用。
shared_array是以引用计数方式共享管理的,可以传递,使用范围更广。

通过get()函数可以取出其管理的内存起始地址,还可以使用operator[]等,就像数组一样使用。

对于非空的vector,可以通过 &(*v.begin()) 取出其管理的内存起始地址

例如:

#include <iostream>
#include <vector>
using namespace std;
 
#include <boost/scoped_array.hpp>
#include <boost/shared_array.hpp>
 
void Print( const char* str )
{
    cout << str << endl;
}
 
void SmartArray( int len )
{
    char src[] = "hello world";
 
    // scoped_array
    boost::scoped_array<char> scope_ary( new char[len] );
    memset( scope_ary.get(), 0, len );
 
    // shared_array
    boost::shared_array<char> share_ary( new char[len] );
    memset( share_ary.get(), 0, len );
   
    memcpy( share_ary.get(), src, sizeof(src) );
 
    Print(share_ary.get());
}
 
void VectorArray( int len )
{
    char src[] = "hello world";
 
    vector<char> vect_ary(len, 0);
    if ( !vect_ary.empty() )
    {
       // 对于非空的vector,可以取出其内存块的地址
       char* base = &(*vect_ary.begin());
 
       memcpy( base, src, sizeof(src) );
       Print(base);
    }
}
 
int main()
{
    SmartArray(100);
    VectorArray(200);
 
    return 0;
}

函数执行前后控制

关于间接运算符operator ->:

  1. 使得对象具有一元后缀间接运算符->,用法就像指针一样,但是行为自定义。
  2. 由于返回类型是自定义的,可以是任何对象类型,包括指针和重载了间接运算符的类型,奇妙的事情出现了,operator-> 会沿着对象序列一直调用(一般直到遇到指针结束),只要经过合适的包装,就能构造出控制执行过程的有用的东西。

先简单分析下operator->的重载的特点:

#include <iostream>
#include <vector>
using namespace std;
 
template<typename T>
struct indirect
{
    T& t_;
    indirect( T& t ) : t_(t) {}
 
    T* operator -> ()
    {
       return &t_;
    }
};
 
int main()
{
    vector<int> v(2, 0);
    indirect< vector<int> > indv(v);
 
    cout << indv->size() << endl;
    cout << (indv.operator->())->size() << endl;
 
    return 0;
}

输出:
2
2

说明:

  1. indv->size()(indv.operator->())->size()等价,这也是对象operator->重载的本质特征。
  2. 通过operator->运算符,可以将完全没有附属关系的类型和成员函数(对象)关联起来(即:indirect<T>类型与size方法),这是C++语法赋予operator->独特的能力。

关于operator->的能力扩展

1、对函数执行进行 — 分析日志,性能分析等

#include <iostream>
#include <string>
using namespace std;
 
template< typename T >
struct indirect
{
    template<typename U>
    struct proxy
    {
       U& u_;
 
       proxy(U& u) : u_(u)
       {
           cout << "prefix" << endl;
       }
 
       ~proxy()
       {
           cout << "suffix" << endl;
       }
 
       U* operator -> ()
       {
           return &u_;
       }
    };
 
    proxy<T> t_;
    indirect( T& t ) : t_(t) {}
 
    proxy<T>& operator -> ()
    {
       return t_;
    }
};
 
struct MyClass
{
    MyClass()
    {
    // 对某个成员函数的调用监控
       indirect<MyClass>(*this) -> init();
    }
 
    void init() { cout << "init" << endl; }
};
 
int main()
{
    string s;
 
    indirect< string >(s) -> push_back( 3 );
    cout << indirect< string const >(s) -> size() << endl;
 
    MyClass mc;
 
    return 0;
}

输出:
prefix
suffix
prefix
1
suffix
prefix
init
suffix

说明:

  1. 辅助类indirect<T>在调用T的成员函数之前和之后都会去调用指定的函数(上面的例子是proxy的构造和析构函数)。
  2. 对于某个成员函数的调用,也可以简单的使用
  3. 还可以继续封装,对模板类indirect增加policy以支持对prefix和suffix的行为,比如可以常用于打日志,分析性能等。

2、由于继承会先初始化基类,借助于继承体系,我们也可以实现的更灵活

#include <iostream>
#include <string>
using namespace std;
 
template<typename U>
struct proxy
{
    U& u_;
 
    proxy(U& u) : u_(u)
    {
       cout << "prefix" << endl;
    }
 
    ~proxy()
    {
       cout << "suffix" << endl;
    }
 
    U* operator -> ()
    {
       return &u_;
    }
};
 
template< typename T >
struct indirect : public proxy<T>
{
    indirect( T& t ) : proxy<T>(t) {}
};
 
int main()
{
    string s;
 
    indirect< string >(s) -> push_back( 3 );
    cout << indirect< string const >(s) -> size() << endl;
 
    return 0;
}

输出:
prefix
suffix
prefix
1
suffix

#include <iostream>
#include <string>
using namespace std;
 
template< typename Next, typename Param >
struct proxy
{
    Param param_;
    proxy( Param param ) : param_(param) {}
 
    Next operator -> ()
    {
       return param_;
    }
};
 
template< typename Next, typename Param >
struct Logger : proxy< Next, Param >
{
    Logger( Param param ) : proxy< Next, Param >( param )
    {
       cout << "begin Logger" << endl;
    }
    ~Logger()
    {
       cout << "end Logger" << endl;
    }
 
};
 
template< typename Next, typename Param >
struct Locking : proxy< Next, Param >
{
    Locking( Param param ) : proxy< Next, Param >( param )
    {
       cout << "begin Locking" << endl;
    }
    ~Locking()
    {
       cout << "end Locking" << endl;
    }
 
};
 
template< typename Start, typename Param >
struct indirect
{
    indirect( Param param ) : param_(param) {}
 
    Start operator -> ()
    {
       return Start(param_);
    }
 
    Param param_;
};
 
int main()
{
    typedef indirect< Locking< Logger<string*, string*>, string* >, string* > wrapper;
 
    string s;
 
    wrapper(&s)->push_back( 'a' );
 
    return 0;
}

输出:
begin Locking
begin Logger
end Logger
end Locking

说明:这是依赖于继承体系下的构造函数顺序和析构函数顺序实现的,在代码执行的前后可以执行一系列独立的监控。

do{...}while(0);

bool Execute()
{
    // 分配资源
    int *p = new int;
 
    bool bOk(true);
    do
    {
       // 执行并进行错误处理
       bOk = func1();
       if(!bOk) break;
 
       bOk = func2();
       if(!bOk) break;
 
       bOk = func3();
       if(!bOk) break;
 
       // ..........
 
    }while(0);
 
    // 释放资源
    delete p;  
    p = NULL;
    return bOk;
}

#define SAFE_DELETE(p) do{ delete p; p = NULL} while(0);

if(NULL != p)
    SAFE_DELETE(p) // 展开自动成为一个语句段

如果没有用do-while,只会执行delete p; 而p = NULL;在之后必定执行,程序出错。

设计最少要求的泛型容器

泛型容器的实现者除了对容器的性能和可用性保证之外,还需要考虑对容纳的泛型元素T的需求,比如,有拷贝构造函数,无异常的析构等,当然,作为设计者,在实现功能的同时,如果可能的话,又要尽可能减少对元素需求的依赖,增强通用性。

举个例子,对于实现一个Stack,操作有:构造、析构、push、top、pop,分析需求:

Stack( int size );                        // 分配内存,不需要构造T
~Stack();                                 // 要求T析构
void push( T const& value );              // 要求T能拷贝构造
T top();                                  // 要求T能拷贝构造
void pop();                               // 要求T析构

因此,总结下来:

实现如下:

#include <algorithm>
 
// 构造对象时才使用 placement new:
template <class T1, class T2>
void construct (T1 &p, const T2 &value)
{
    // T must support copy-constructor
    new (&p) T1(value); 
}
 
// 显式的调用析构函数
template <class T>
void destroy (T const &t)  throw ()
{
    // T must support non-throwing destructor
    t.~T(); 
}
 
template<class T>
class Stack
{
public:
    Stack (int size=10)
       : size_(size),
       // T need not support default construction
       array_ (static_cast <T *>(::operator new (sizeof (T) * size))),
       top_(0)
    { }
 
    void push (const T & value)
    {
       // T need not support assignment operator.
       construct (array_[top_++], value);
    }
    T top ()
    {
       return array_[top_ - 1]; // T should support copy construction
    }
    void pop()
    {
       destroy (array_[--top_]);     // T destroyed
    }
    ~Stack () throw()
    {
       //剩余的元素都析构,并用operator delete释放operator new申请的内存
       std::for_each(array_, array_ + top_, destroy<T>);
       ::operator delete(array_); // Global scope operator delete.
    }
 
private:
    int size_;
    T * array_;
    int top_;
};
 
class X
{
public:
    X (int) {} // No default constructor for X.
private:
    X & operator = (const X &); // assignment operator is private
};
 
int main (void)
{
    Stack <X> s; // X works with Stack!
 
    return 0;
}

编译成功,说明Stack容器对于没有构造函数和赋值运算符的类型也支持。

关于new与delete

new operator:

如果创建的是简单类型(如char)的变量,那么第二步会被省略。

比如: CTest* pT = new CTest(1, 2);

它的调用实际上等效于:

void*  p  = operator new( sizeof(CTest) );
CTest* pT = new(p) CTest(2, 2);

其中前一句是operator new分配内存,后一句是placement new调用构造函数,并返回正确的CTest*指针。

operator new:

操作符new,原型为:void* operator new(size_t size);

这种用法和调用 malloc 一样, 只分配了sizeof(CTest)大小的内存。

placement new:

置换new,它在一块指定的内存上调用构造函数, 包含头文件<new>之后也可以直接使用,如:CTest* pT = new(p) CTest(2, 2);

它在p这块内存上调用CTest的构造函数来初始化CTest。如果用 placement new 构造出来的对象,必须显示的调用对象的析构函数,如:(因为构造的时候是operator new创建,需要用operator delete释放,而不能用delete,需要显式的调用析构函数)`pT->~CTest();` 然后释放能存, 调用 operator delete (对应于分配时的 operator new)`operator delete(pT);`

delete operator:

operator delete:

同理,对应于分配内存的 operator new,释放内存的为 operator delete ,它也可以被重载。

new、delete的数组操作版本:operator new []operator delete [] 也可以直接被重载。

虚函数表的映射技巧

COM接口都是纯虚基类,一个组件可以实现多个COM接口,实现它的纯虚函数;而且COM接口的复用方面可以实现用包容和聚合:包容的实现比较简单,只要维护一个内部组件的接口指针即可,内部组件的接口都只有外部组件在使用,客户不直接使用;但是要实现聚合就比较麻烦,因为对客户的请求,外部组件可能直接抛出内部组件的接口,而使用这个内部组件的接口又能QueryInterface得到外部组件的接口。

常用的实现方式是内部组件实现“两个”IUnknown接口。其实是实现了一份IUnknown接口和一份INondelegatingUnknown接口,设计上,让它们具有相同的虚函数表结构,具体的实现可以参考《COM技术内幕》。

看一个例子:

#include <iostream>
using namespace std;
 
struct IUnknown
{
    virtual void AddRef() = 0;
    virtual void Release( string const& s ) = 0;
    virtual void QueryInterface( int, void** ) = 0;
};
 
struct IDelegatingUnknown
{
    virtual void DelegatingAddRef() = 0;
    virtual void DelegatingRelease( string const& s ) = 0;
};
 
struct Test
    : public IDelegatingUnknown
{
    //将IDelegatingUnknown接口指针,强制转化为没有任何关系的IUnknown的接口指针
    IUnknown* GetUnknown()
    {
       return reinterpret_cast<IUnknown*>( static_cast<IDelegatingUnknown*>(this) );
    }
 
    // 实现IDelegatingUnknown接口
    virtual void DelegatingAddRef()
    {
       cout << "Delegating AddRef" << endl;
    }
 
    virtual void DelegatingRelease( string const& s )
    {
       cout << "DelegatingRelease: " << s << endl;
    }
};
 
int main()
{
    Test t;
    IUnknown* pn = t.GetUnknown();
 
    // 真正调用的是IDelegatingUnknown的虚函数实现Test::DelegatingAddRef
    pn->AddRef();
    pn->Release( "release" );
 
    return 0;
}

输出:
Delegating AddRef
DelegatingRelease: release

分析:

对于pn->AddRef();通过IUnknown的声明可以看到,将调用虚函数表的第一个函数,但是由于pn当前真正指向的是IDelegatingUnknown的指针,因此将调用IDelegatingUnknown::DelegatingAddRef,其内存对应的值为static_cast<IDelegatingUnknown*>(this),因此最终调用IDelegatingUnknown的第1个纯虚函数实现Test::DelegatingAddRef