避免返回handles指向对象内部成分——条款28

        假设你的程序涉及矩形。每个矩形由其左上角和右下角表示。为了让一个Rectangle对象尽可能小,你可能会决定不把定义矩形的这些点放在Rectangle对象内,而是放在一个辅助的struct内再让Rectangle去指它:

class Point {        // 这个class用来表述“点”
public:
	Point(int x, int y);
	...
	void setX(int newVal);
	void SetY(int newVal);
	...
};
struct RectData {        // 这些“点”数据用来表现一个矩形
	Point ulhc;          // ulhc="upper left-hand comer"(左上角)
	Point lrhc;          // ulhc="lower right-hand comer"(右下角)
};
class Rectangle {
	...
private:
	std::tr1::shared_ptr<RectData> pData;    // 关于std::tr1::shared_ptr,见条款3
};

        Rectangle的客户必须能够计算Rectangle的范围,所以这个class提供upperLeft函数和lowerRight函数。Point是个用户自定义类型,所以根据条款20给我们的忠告(它说以by reference方式传递用户自定义类型往往比以by value方式传递更高效),这些函数于是返回reference,代表底层的Point对象:

class Rectangle {
public:
	...
	Point& upperLeft() const { return pData->ulhc; }
	Point& lowerRight() const { return pData->lrhc; }
	...
};

        这样的设计可通过编译,但却是错误的。实际上它是自我矛盾的。一方面upperLeft和lowerRight被声明为const成员函数,因为它们的目的只是为了提供客户一个得知Rectangle相关坐标点的方法,而不是让客户修改Rectangle(见条款3)。另一方面两个函数却都返回references指向private内部数据,调用者于是可通过这些references更改内部数据!例如:

Point coord1(0, 0);
Point coord2(100, 100);
const Rectangle rec(coord1, coord2);    // rec是个const矩形,从(0, 0)到(100, 100)
rec.upperLeft().setX(50);               // 现在rec却变成从(50, 0)到(100, 100)

        这里请注意,upperLeft的调用者能够使用被返回的reference(指向rec内部的Point成员变量)来更改成员。但rec其实应该是不可变的(const)!

      这立刻给我们两个教训。第一,成员变量的封装性最多只等于“返回其reference”的函数的访问级别。本例之中虽然ulhc和lrhc都被声明为private,它们实际上却是public,因为public函数upperLeft和lowerRight传出了它们的references。第二,如果const成员函数传出一个references,后者所指数据与对象自身有关联,而它又被存储于对象之外,那么这个函数的调用者可以修改那笔数据。这正是bitwise constness的一个附带结果,见条款3.

        上面所说的每件事情都是由于“成员函数返回references”。如果他们返回的是指针或迭代器,相同的情况还是发生,原因也相同。References、指针和迭代器统统都是所谓的handles(号码牌,用来取得某个对象),而返回一个“代表对象内部数据”的handle,随之而来的便是“降低对象封装性”的风险。同时,一如稍早所见,它也可能导致“虽然调用const成员函数却造成对象状态被更改”。

        通常我们认为,对象的“内部”就是指它的成员变量,但其实不被公开使用的成员函数(也就是被声明为protected或private者)也是对象“内部”的一部分。因此也应该留心不要返回它们的handles。这意味你绝对不该令成员函数返回一个指针指向“访问级别较低”的成员函数。如果你那么做,后者的实际访问级别就会提供高如同前者(访问级别较高者),因为客户可用取得一个指针指向那个“访问级别较低”的函数,然后通过那个指针调用它。

        然而“返回指针指向某个成员函数”的情况毕竟不多见,所以让我们把注意力收回,专注于Rectangle class和它的upperLeft以及lowerRight成员函数。我们在这些函数身上遭遇的两个问题可以轻松去除,只要对它们的返回类型加上const即可:

class Rectangle {
public:
	...
	const Point& upperLeft() const { return pData->ulhc; }
	const Point& lowerRight() const { return pData->lrhc; }
	...
};

        有了这样的改变,客户可以读取矩形的Points,但不能涂写它们。这意味当初声明upperLeft和lowerRight为const不再是个谎言,因为它们不再允许客户更改对象状态。

        但即使如此,upperLeft和lowerRight还是返回了“代表对象内部”的handles,有可能在其它场合带来问题。更明确地说,它可能导致dangling handles(空悬的号码牌):这种handles所指东西(的所属对象)不复存在。这种“不复存在的对象”最常见的来源就是函数返回值。例如某个函数返回GUI对象的外框(bounding box),这个外框采用矩形形式:

class GUIObject { ... };
const Rectangle                         // 以by value方式返回一个矩形
	boundingBox(const GUIObject& obj);  // 条款3谈过为什么返回类型是const

        现在,客户有可能这么使用这个函数:

GUIObject* pgo;                         // 让pgo指向某个GUIObject
...
const Point* pUpperLeft =               // 取得一个指针指向外框左上点
	&(boundingBox(*pgo).upperLeft());

        对boundingBox的调用获得一个新的、暂时的Rectangle对象。这个对象没有名字,所以我们权且称它为temp。随后upperLeft作用于temp身上,返回一个reference指向temp的一个内部成分,更具体地说是指向一个用以标示temp的Points。于是pUpperLeft指向那个Point对象。目前为止一切还好,但故事尚未结束,因为那个语句结束之后,boundingBox的返回值,也就是我们所说的temp将被销毁,而那间接导致temp内的Points析构。最终导致pUpperLeft指向一个不再存在的对象:也就是说一旦产出pUpperLeft的那句语句结束,pUpperLeft也就变成空悬、虚吊(dangling)!

        这就是为什么函数如果“返回一个handle代表对象内部成分”总是危险的原因。无论这所谓的handle是个指针或迭代器或reference,也不论这个handle是否为const,也不论那个返回handle的成员函数是否为const。这里唯一关键是,有个handle被传出去了,一旦如此你就是暴露在“handle比其所指对象更长寿”的风险下。

        这并不意味你绝对不可以让成员函数返回handle。有时候你必须那么做。例如operator[]就允许你“摘采”strings和vectors的个别元素,而这些operator[]s就是返回references指向“容器内的数据”,那些数据会随着容器被销毁而销毁。尽管如此,这样的函数毕竟是例外,不是常态。

        完整代码:

#include <iostream>
#include <memory>
using namespace std;

class Point {        // 这个class用来表述“点”
public:
	Point(int x, int y)
		: x(x)
		, y(y)
		{}
	void setX(int newVal) {
		x = newVal;
	}
	void setY(int newVal) {
		y = newVal;
	}
	const int getX() const{
	    return x;
	}
private:
	int x;
	int y;
};
struct RectData {        // 这些“点”数据用来表现一个矩形
	Point ulhc;          // ulhc="upper left-hand comer"(左上角)
	Point lrhc;          // ulhc="lower right-hand comer"(右下角)
	RectData(Point ul, Point lr)
	:ulhc(ul)
	,lrhc(lr)
	{
	    cout<<"RectData construct"<<endl;
	}
};
class Rectangle {
public:
    Rectangle(Point p1, Point p2)
    : pData(new RectData(p1, p2))
    {
        cout<<"Rectangle construct"<<endl;
    }
	const Point& upperLeft() const { return pData->ulhc; }
	const Point& lowerRight() const { return pData->lrhc; }
private:
	shared_ptr<RectData> pData;    // 关于std::tr1::shared_ptr,见条款3
};

int main() {
    Point coord1(0, 0);
    Point coord2(100, 100);
    const Rectangle rec(coord1, coord2);    // rec是个const矩形,从(0, 0)到(100, 100)
    rec.upperLeft().setX(50);               // 现在rec却变成从(50, 0)到(100, 100)
    cout<<"point left x = "<<rec.upperLeft().getX()<<endl;  // error
    return 0;
}

        上述用 upperLeft()函数用 const 修饰之后再调用 rec.upperLeft().setX(50); 时报错:error: passing 'const Point' as 'this' argument discards qualifiers [-fpermissive]

空悬指针验证:

#include <iostream>
#include <memory>
using namespace std;

class Point {        // 这个class用来表述“点”
public:
	Point(int x, int y)
		: x(x)
		, y(y)
		{}
	void setX(int newVal) {
		x = newVal;
	}
	void setY(int newVal) {
		y = newVal;
	}
	const int getX() const{
	    return x;
	}
private:
	int x;
	int y;
};
struct RectData {        // 这些“点”数据用来表现一个矩形
	Point ulhc;          // ulhc="upper left-hand comer"(左上角)
	Point lrhc;          // ulhc="lower right-hand comer"(右下角)
	RectData(Point ul, Point lr)
	:ulhc(ul)
	,lrhc(lr)
	{
	    cout<<"RectData construct"<<endl;
	}
};
class Rectangle {
public:
    Rectangle(Point p1, Point p2)
    : pData(new RectData(p1, p2))
    {
        cout<<"Rectangle construct"<<endl;
    }
    ~Rectangle() {
        cout<<"Rectangle deconstruction"<<endl;
    }
	const Point& upperLeft() const { return pData->ulhc; }
	const Point& lowerRight() const { return pData->lrhc; }
private:
	shared_ptr<RectData> pData;    // 关于std::tr1::shared_ptr,见条款3
};

const Rectangle getRectangle() {
    Point coord1(10, 0);
    Point coord2(100, 100);
    const Rectangle rec(coord1, coord2); 
    return rec;
}
int main() {
    const Point* p = &getRectangle().upperLeft();
    cout<<"point p = "<<p->getX()<<endl;   
    Point coord1(10, 0);
    Point coord2(100, 100);
    const Rectangle rec(coord1, coord2);    // rec是个const矩形,从(0, 0)到(100, 100)
    const Point* pl = &rec.upperLeft();               // 现在rec却变成从(50, 0)到(100, 100)
    cout<<"point pl = "<<pl->getX()<<endl;
    // cout<<"point left x = "<<rec.upperLeft().getX()<<endl;
    
    return 0;
}

输出结果:

RectData construct
Rectangle construct
Rectangle deconstruction    // temp对象用完就会被销毁
point p = 0                // 所以指针指向的内容是空的
RectData construct
Rectangle construct
point pl = 10
Rectangle deconstruction

请记住:

  • 避免返回handles(包括references、指针、迭代器)指向对象内部。遵守这个条款可增加封装性,帮助const成员函数的行为像个const,并将发生“虚吊号码牌”(dangling handles)的可能性降至最低。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值