More Effective C++ 条款30 Proxy classes(替身类,代理类)

1. 所谓代理类(proxy class),指的是"它的每一个对象都是为了其他对象而存在的,就像是其他对象的代理人一般".某些情况下用代理类取代某些内置类型可以实现独特的功能,因为可以为代理类定义成员函数而但却无法对内置类型定义操作.条款5就展示了一个使用代理类阻止隐式类型转换的例子.

2. 实现二维数组.

C++没有提供分配动态二维数组的语法,因此常常需要定义一些类(模板实现这些功能),像这样:

template<class T>
class Array2D {
public:
    Array2D(int dim1, int dim2);
    ...
};

既然是二维数组,那么有必要提供使用"[][]"访问元素的操作,然而[][]并不是一个操作符,C++也就不允许重载一个operator[][],解决办法就是采用代理类,像这样:

template<class T>
class Array2D {
public:
    //代理类
    class Array1D {
    public:
        T& operator[](int index);
        const T& operator[](int index) const;
    ...
    };
    Array1D operator[](int index);
    const Array1D operator[](int index) const;
    ...
};

那么以下操作:

Array2D<float> data(10, 20);
...
cout << data[3][6];

data[3][6]实际上进行了两次函数调用:第一次调用Array2D的operator[],返回Array1D对象,第二次调用Array1D的operator[],返回指定元素.

(本例对proxy class作用的体现其实并不显著,因为可以对指针施加operator[],因此只要使Array2D的oeprator[]返回一个指针可达到同样效果,没有必要使用代理类)

3. 区分operator[]的读写动作

条款29用String类的例子讨论了引用计数,由于当时无法判断non-const版本oeprator[]返回的字符将被用于读操作还是写操作,因此保险起见,一旦调用non-const版本operator[],便开辟一块新内存并复制数据结构到新内存.在这种策略下,因此如果operator[]返回的字符被用于读操作,那么分配新内存并复制数据结构的行为其实是不必要的,由此会带来效率损失,使用proxy class便可以做到区分non-const operator[]用于读还是写操作,像这样:

class String {
public:
    //代理类用于区分operator[]的读写操作
    class CharProxy { // proxies for string chars
    public:
        CharProxy(String& str, int index); // creation
        CharProxy& operator=(const CharProxy& rhs); // lvalue
        CharProxy& operator=(char c); // uses
        operator char() const;
    private:
        String& theString; //用于操作String,并在适当时机开辟新内存并复制
        int charIndex;
    };
    const CharProxy operator[](int index) const; // for const Strings
    CharProxy operator[](int index); // for non-const Strings
    ...
    friend class CharProxy;
private:
    RCPtr<StringValue> value;//见条款29
};

对String调用operator[]将返回CharProxy对象,CharProxy通过重载oeprator char模拟char类型的行为,但它比char类型更有优势——可以为CharProxy定义新的操作,这样当对CharProxy使用operator=时,便可以得知对CharProxy进行写操作,由于CHarProxy保存了父对象String的一个引用,便可以在现在执行开辟内存并复制数据结构的行为,像这样:

String::CharProxy& String::CharProxy::operator=(const CharProxy& rhs)
{
    if (theString.value->isShared()) {
        theString.value = new StringValue(theString.value->data);
    }
    theString.value->data[charIndex] = rhs.theString.value->data[rhs.charIndex];
    return *this;
}
String::CharProxy& String::CharProxy::operator=(char c)
{
    if (theString.value->isShared()) {
        theString.value = new StringValue(theString.value->data);
    }
    theString.value->data[charIndex] = c;
    return *this;
}
//以上来那个函数的代码部分有重复,可考虑将重复部分提取成一个函数

由于内存开辟和数据结构赋值任务交由CharProxy完成,String的operator[]相当简单,像这样:

const String::CharProxy String::operator[](int index) const
{
    return CharProxy(const_cast<String&>(*this), index);
}
String::CharProxy String::operator[](int index)
{
    return CharProxy(*this, index);
}

CharProxy实现的其他部分如下:

String::CharProxy::CharProxy(String& str, int index): theString(str), charIndex(index) {}
String::CharProxy::operator char() const
{
return theString.value->data[charIndex];
}

4. 局限性.

就像智能指针永远无法完全取代内置指针一样,proxy class也永远无法模仿内置类型的所有特点.proxy class可以实现内置类型无法做到功能,但有利有弊——为了模仿内置类型的其他特点,它还要打许多"补丁".

1). 对proxy class取址.

条款29通过为StringValue类添加可共享标志(flag)来表示对象是否可被共享以防止外部指针的篡改,其中涉及到对operator[]返回值进行取址操作,这就提示CharProxy也需要对operator&进行重载,像这样:

class String {
public:
    class CharProxy {
    public:
        ...
        char * operator&();
        const char * operator&() const;
        ...
    };
    ...
};

const版本operator&实现比较容易:

const char * String::CharProxy::operator&() const
{
    return &(theString.value->data[charIndex]);
}

non-const版本的operator&要做的事情多一些:

char * String::CharProxy::operator&()
{
    //如果正在使用共享内存,就开辟新内存并复制数据结构
    if (theString.value->isShared()) {
        theString.value = new StringValue(theString.value->data);
    }
    //由于有外部指针指向它,因此有被篡改风险,禁止使用共享内存
    theString.value->markUnshareable();
    return &(theString.value->data[charIndex]);
}

2). 将proxy class传递给接受"references to non-const objects"的函数.

假设有一个swap函数用于对象两个char的内容:

void swap(char& a, char& b);

那么将无法将CharProxy做参数传递给swap,因为swap的参数是char&,尽管CharProxy可以转换到char,但由于抓换后的char是临时对象,仍然无法绑定到char&,解决方法似乎只有对swap进行重载.

3). 通过proxy cobjects调用真实对象的member function.

如果proxy class的作用是用来取代内置类型,那么它必须也应该对内置类型能够进行的操作进行重载,如++,+=等,如果它用来取代类类型,那么它也必须具有相同成员函数,使得对该类类型能够进行的操作同样也能够施行于proxy class.

4). 隐式类型转换.

proxy class要具有和被代理类型相同的行为,通常的做法是重载隐式转换操作符,正如条款5对proxy class的使用那样,proxy class可以利用"用户定制的隐式类型转换不能连续实行两次"的特点阻止不必要的隐式类型转换,proxy class同样可能因为这个特点而阻止用户需要的隐式类型转换.

5. 评估

proxy class的作用很强大,像上面所提到的实现多维数组,区分operator[]的读写操作,压抑隐式类型转换等,但是也有其缺点,如果函数返回proxy class对象,那么它僵尸一个临时对象,产生和销毁它就有可能带来额外的构造和析构成本,此外正如4所讲,proxy class无法完全代替真正对象的行为,尽管大多数情况下真正对象的操作都可由proxy class完成.

时间: 2024-10-28 15:22:11

More Effective C++ 条款30 Proxy classes(替身类,代理类)的相关文章

探索Mybatis之JDK动态代理:探究Proxy.newProxyInstance()生成的代理类解析

Mybatis的Mapper接口UserMapper 1 package com.safin.Mapper; 2 3 import com.safin.Pojo.User; 4 5 import java.util.List; 6 7 public interface UserMapper { 8 int insertUser(User user); 9 User getUser(Long id); 10 List<User> findUser(String userName); 11 } 我

Effective C++ 条款30 透彻了解inlining的里里外外

1. inline函数既和带参宏一样不带来函数调用的额外开销,又具有和非inline函数相同的功能,也就是说,inline函数同时具备带参宏和非inline函数的优点. 此外,编译器优化机制通常针对于那些不含参数调用的代码,因此inline某个函数就有可能使编译器对它执行语句相关最优化. 2. 虽然inline函数有诸多优点,但由于inline函数对每一次函数调用都用函数本体替换,这无疑加重了编译负担,更重要的是它增加了代码量,此外,由于inline造成的代码膨胀"会导致额外的换页行为(pagi

Effective C++:条款30:透彻了解inlining的里里外外

(一) inline函数,可以调用它们而又不需蒙受函数调用所招致的额外开销. inline函数背后的整体观念是,将"对此函数的每一个调用"都已函数本体替换之,这样做可能增加你的目标码(object code)大小.在内存有限的机器上,过度inline会造成程序体积太大,导致换页行为,降低缓存的命中率等一些带来效率损失的行为.如果inline函数的本体很小,编译器针对"函数本体"所产生的码可能比针对"函数调用"所产出的码更小.将函数inline可以

More Effective C++ 条款17 考虑使用lazy evaluation(缓式评估)

1. lazy evaluationg实际上是"拖延战术":延缓运算直到运算结果被需要为止.如果运算结果一直不被需要,运算也就不被执行,从而提高了效率.所谓的运算结果不被执行,有时指只有部分运算结果被需要,那么采用拖延战术,便可避免另一部分不被需要的运算,从而提高效率,以下是lazy evaluation的四种用途. 2. Reference Counting(引用计数) 如果要自己实现一个string类,那么对于以下代码: String s1="Hello"; S

effective c++ 条款18 make interface easy to use correctly and hard to use incorrectly

举一个容易犯错的例子 class Date { private: int month; int day; int year; public: Date(int month,int day,int year) { this->month = month; ... } } //wrong example Date date(30,3,1995);//should be 3,30 Date date(2,30,1995);//should be 3,30 使用类型可避免这个问题 class Month

effective c++条款26-31“class and function的实现”整理

一.类的实现面临的问题: 太快定义变量可能造成效率上的拖延:过度使用转型(casts)可能导致代码变慢又难维护,又招来微妙难解的错误:返回对象"内部数据之号码牌(handls)"可能会破坏封装并留给客户虚吊号码牌:为考虑异常带来的冲击则可能导致资源泄漏和数据败坏:过度热心地inlining可能引起代码膨胀:过度耦合则可能导致让人不满意的冗长建置时间. 二.条款26:尽可能延后变量定义式的出现时间 有些对象,你可能过早的定义它,而在代码执行的过程中发生了导常,造成了开始定义的对象并没有被

Effective C++ 条款七 为多态基类声明virtual析构函数

class TimeKeeper { public: TimeKeeper(); // ~TimeKeeper(); 错误,此作为一个基类,被继承了.其继承类被delete后,基类被销毁,但继承类可能没被销毁 virtual ~TimeKeeper();//必须声明为virtual类型才可以. protected: private: };   class AtomicClock: public TimeKeeper{}; //继承   TimeKeeper* ptk = getTimeKeepe

Effective C++笔记_条款43 学习处理模板化基类内的名称

开篇就来了一个示例代码,整个这个小节就围绕这个示例代码来描述模板化基类内的名称(函数).主要是因为C++知道base class templates有可能被特化,而那个特化版本肯呢个不提供和一般性template相同的接口.因此它往往拒绝在templatized base classes(模板化基类)内寻找继承而来的名称. 1 class CompanyA { 2 public: 3 //... 4 void sendCleartext(const std::string& msg); 5 vo

More Effective C++ 条款35 让自己习惯于标准C++ 语言

(由于本书出版于1996年,因此当时的新特性现在来说可能已经习以为常,但现在重新了解反而会起到了解C++变迁的作用) 1. 1990年后C++的重要改变 1). 增加了新的语言特性:RTTI,namespaces,bool,关键词mutable和explicit,enums作为重载函数之自变量所引发的类型晋升转换,以及"在class 定义区内直接为整数型(intergral) const static class members设定初值"的能力. 2). 扩充了Templates的特性