[读书笔记] -《C++ API设计》第7章 性能

1、前置声明

头文件A包含另一个头文件B,是为了引入在头文件A中使用到的类、函数、结构体、枚举或其他实体的声明。在面向对象程序中,最常见的情况是头文件A想要引入头文件B中一个或多个类的声明。不过,很多情况下头文件A实际上不必包含头文件B,而只需要提供所需类的前置声明即可。前置声明可以在下列几种情况下使用。

        1>不需要知道类的大小。如果包含的类要作为成员变量或打算从包含类派生子类,那么编译器需要知道类的大小。

        2>没有引用类的任何成员方法。引用类的成员方法需要知道方法原型,即参数和返回值类型。

        3>没有引用类的任何成员变量。不过决不应把类的成员变量声明为公有或受保护的。

例如,如果头文件A仅仅通过指针或引用指向了头文件B中的一些类名,那么可以使用前置声明。

class B; //前置声明

class A
{
public:
    void SetObject(const B& obj);
private:
    B* mObj;
};

如果要修改类A的定义是的编译器知道类B的实际大小,那么必须包含类B实际的声明,即必须包含B的头文件。比如,在A中存储B的一份实际副本的情况。

#include <B.h>

class A
{
public:
    void SetObject(const B& obj);
private:
    B mObj;
};

很明显,如果要使用另一个头文件中的类,就需要先在.cpp文件中包含那个头文件,例如,A.cpp必须包含B.h。前置声明仅仅告诉编译器把名字加入它的符号表,并承诺在实际使用那个名字时提供完整的声明。

值得注意的是,如果以传值方式给方法传递变量,或者以传值方式返回值,(但应该尽量选择const引用而不是以传值方式传递参数。)那么前置声明即可满足需求。也许你认为编译器此时需要知道类的大小,但实际上只有实现方法的代码和任何调用它的客户代码需要知道类的大小。所以下面这个例子是合法的:

class B;

class A
{
public:
    void SetObject(B obj);
    B GetObject() const;
};

总结:一般来说,只有在自己的类中将某个类的对象作为数据成员使用时,或者需要继承某个类时,才应该包含那个类的头文件。

 

2、冗余的#include警戒语句

降低过多包含文件解析开销的另一种方法是,在包含点增加冗余预处理警戒语句。例如,对于一个名为bigfile.h的包含文件,它看上去是这样的:

#ifndef BIGFILE_H
#define BIGFILE_H

//大量代码

#endif

你可以在另一个头文件中以如下方式包含该文件:

#ifndef BIGFILE_H
#include "bigfile.h"
#endif

如果已包含了整个包含文件,那么将省去打开和解析该文件的无益成本。对于比较小的包含层次结构,这看上去也许是无关紧要的优化;但是对于具有很多包含文件的代码库,这项优化会带来明显的性能差异。

 

3、constexpr关键字

constexpr提供编译时优化,它可以用来标识已知为恒定不变的函数或变量,以便编译器执行更好的优化。例如,

int GetTableSize(int elems) { return elems * 2; }
double myTable[GetTableSize(2)]; // 在C++98中非法

依据C++98标准,这是非法的,因为编译器无从得知GetTableSize()返回的值是编译时常量。不过在新的C++规范中,你可以告诉编译器实际上它们就是常量:

constexpr int GetTableSize(int elems) { return elems * 2; }
double myTable[GetTableSize(2)]; // 在C++0x中合法

constexpr关键字也可以应用于变量。constexpr关键字能够标识函数结果是编译时常量,因此我们可以通过函数调用定义常量,同时客户也可以在编译时使用这些常量值。例如:

// myapi.h (仅在C++0x中可行)
class MyAPI
{
public:
    constexpr int GetMaxNameLength() { return 128; }
    constexpr int GetMaxRecords() { return 65525; }
    constexpr std::string GetLogFilename() { return "filename.log"; }
};

 

4、使用构造函数初始化列表,从而为每个数据成员减少一次调用构造函数的开销。这些应在.cpp文件中声明,以便隐藏实现细节。例如,

// avatar.h
class Avatar
{
public:
    Avatar(const std::string& first, const std::string& last);

private:
    std::string mFirstName;
    std::string mLastName;
};

然后在相关联的.cpp文件中提供构造函数和初始化列表:

// avatar.cpp
Avatar::Avatar(const std::string& first, const std::string& last)
    mFirstName(first),
    mLastName(last)
{
}

 

5、写时复制

节省内存最好的办法之一是到确实需要时再分配。这是写时复制技巧的本质目标。其方法是允许所有客户共享一份唯一的资源,直到他们中的一个需要修改这份资源为止。只有在那个时间点才会构造副本——这是“写时复制”名字的来由。其优势在于如果资源从没有被修改,那么就可以被所有客户共享。这个享元(Flyweight)设计模式相关,该模式描述了对象应该共享尽可能多的内存,从而最小化内存消耗。

有若干种实现写时复制的方法。一种流行的方法是像创建共享指针或弱指针模板那样,声明允许创建指针的模板,这些指针指向写时复制语义所管理的对象。这种类通常包含一个标准的共享指针,用来跟踪底层对象的引用计数;同时为那些需要修改对象的操作提供私有的Detach()方法,以便从共享对象分离并创建一份新的副本。下面的实现使用了Boost共享指针。为清楚起见,该示例在类声明中使用了内联方法。在现实应用中,应该在单独的头文件中隐藏这些内联定义,因为这会使接口声明语义不清。

#include <boost/shared_ptr.hpp>

template<class T>
class CowPtr
{
public:
    typedef boost::shared_ptr<T> RefPtr;

    inline CowPtr() : mPtr(0) {}
    inline ~CowPtr() {}
    inline explicit CowPtr(T* other) : mPtr(other) {}
    inline CowPtr(const CowPtr<T>& other) : mPtr(other.mPtr) {}

    inline T& operator * ()
    {
        Detach();
        return *mPtr.get();
    }
    inline const T& operator * () const
    {
        return *mPtr.get();
    }
    inline T* operator -> ()
    {
        Detach();
        return mPtr.get();
    }
    inline const T* operator -> () const
    {
        return mPtr.get();
    }
    inline operator T* ()
    {
        Detach();
        return mPtr.get();
    }
    inline operator const T* () const
    {
        Detach();
        return mPtr.get();
    }
    inline T* data()
    {
        Detach();
        return mPtr.get();
    }
    inline const T* data() const
    {
        return mPtr.get();
    }
    inline const T* constData() const
    {
        return mPtr.get();
    }
    inline bool operator == (const CowPtr<T>& other) const
    {
        return mPtr.get() == other.mPtr.get();
    }
    inline bool operator != (const CowPtr<T>& other) const
    {
        return mPtr.get() != other.mPtr.get();
    }
    inline bool operator ! () const
    {
        return !mPtr.get();
    }
    inline CowPtr<T>& operator = (const CowPtr<T>& other)
    {
        if(other.mPtr != mPtr) {
            mPtr = other.mPtr;
        }
        return *this;
    }
    inline CowPtr& operator = (T* other)
    {
        mPtr = RefPtr(other);
        return *this;
    }

private:
    inline void Detach()
    {
        T* temp = mPtr.get();    
        if(temp && !mPtr.unique()) {
            mPtr = RefPtr(new T(*temp));
        }
    }

    RefPtr mPtr;
};

这个类可以这样使用:

CowPtr<std::string> string1(new std::string("Share Me"));
CowPtr<std::string> string2(string1);
CowPtr<std::string> string3(string1);
string3->append("!");

这个例子中,string2和string1指向相同的对象,而string3指向对象的一份副本,因为它需要修改对象。std::string的很多实现已经采用了写时复制,这里实现string只是为了举例方便。

上面展示的CowPtr实现中可以找到一处漏洞。用户能够发掘出写时复制指针并由此访问底层对象,从而获得数据的引用。然后可以直接修改数据,并因此影响所有共享该对象的CowPtr变量。例如,

CowPtr<std::string> string1(new std::string("Share Me"));
char& char_ref = string1->operator[](1);
CowPtr<std::string> string2(string1);
char_ref = 'p';

这段代码中,用户取得了指向string1底层的std::string中一个字符的引用。string2创建后,它与string1共享相同的内存。用户随后直接修改了共享字符串中的第二个字符,导致string1和string2都等于Spare Me。

避免这类滥用的最好的做法是不要向客户暴露CowPtr。大多数情况下,不需要让客户意识到你正在使用写时复制优化这一事实:毕竟这属于实现细节。相反,应该在你的对象中使用CowPtr声明成员变量,而不要以任何方式改变公有API。这个Qt库中称为隐式共享。例如,

// myobject.h
class MyObject
{
public:
    MyObject();

    std::string GetValue() const;
    void SetValue(const std::string& value);

private:
    CowPtr<std::string> mData;
};

MyObject的实现如下所示:

// myobject.cpp
MyObject::MyObject() : mData(0) {}

std::string MyObject::GetValue() const
{
    return (mData) ? *mData : "";
}

void MyObject::SetValue(const std::string& value)
{
    mData = new std::string(value);
}

这样客户就可以在对写时复制毫不知情的情况下使用MyObject API。而潜在的事实是:对象只要有可能就会共享内存,从而带来更高效的复制和赋值操作。

MyObject obj1;
obj1.SetValue("Hello");
MyObject obj2 = obj1;
std::string val = obj2.GetValue();
MyObject obj3 = obj1;
obj3.SetValue("There");

上面这个例子中,obj1和obj2共享相同的底层string对象,而obj3包含自己的string副本,因为它修改了这个string。

 

6、对迭代器应尽量使用前置自增操作符(++it),而非后置自增操作符(it++),以避免临时对象的构造和析构。

 

7、随机访问。支持随机访问的STL容器类为此提供了两种访问方式。

        1>[]操作符。这个操作符的实现通常不做边界检查,所以可以非常高效地实现。

        2>at()方法。这个方法需要检查所提供的索引是否越界,如果越界则抛出异常。因此,这种做法会比[]操作符慢。

 

8、声明常量

1>在头文件中用extern声明常量:

extern const int MAX_NAME_LENGTH;
extern const float LOG_2E;
extern const std::string LOG_FILENAME;

然后在相应的.cpp文件定义每个常量的值。通过这种方式,变量的空间就只会被分配一次。这样做还有另外一个优势,就是在头文件中隐藏实际的常量值,毕竟它们属于实现细节。

2>更好的办法是在类中声明常量,并将其声明为静态const。(这样它们就不会被计入每个对象的内存大小了。)

// myapi.h
class MyAPI
{
public:
    static const int MAX_NAME_LENGTH;
    static const int MAX_RECORDS;
    static const std::string LOG_FILENAME;
};

随后可以在相关联的.cpp文件中定义这些常量的的值。

// myapi.cpp
const int MyAPI::MAX_NAME_LENGTH = 128;
const int MyAPI::MAX_RECORDS = 65525;
const std::string MyAPI::LOG_FILENAME = "filename.log";

某些情况下,避免文件膨胀问题的另一种方法是使用枚举替代变量,或者使用getter方法返回常量值。

 // myapi.h
class MyAPI
{
public:
    static int GetMaxNameLength();
    static int GetMaxRecords();
    static std::string GetLogFilename();
};

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值