Data语义学——Data Member的绑定

考虑以下程序:

1
2
3
4
5
6
7
8
9
10
extern float x;//外部的x变量
class Point3d{
public:
Point3d(float,float,float);
float X() const {return x;}
void X(float new_x) const {x=new_x;}
...
private:
float x,y,z;
};

如果我们调用Point3d::X(),那么它返回的是Point3d::x还是extern x?熟悉C++的人都明白是前者,但并非一直以来都是前者。

在C++最早的编译器上,如果在Point3d::X()的两个函数实例中对x进行参阅(取用)操作,这操作将会指向global x object。这种结果基本上不在开发者的预期之内,因此直接导致了C++早期的两种防御性程序设计风格:

  1. 将所有的data members放在class声明起头处,以确保正确的绑定:
    1
    2
    3
    4
    5
    class Point3d{
    float x,y,z;
    public:
    ...
    }
  2. 将所有的inline functions无论大小都置于声明之外
    1
    2
    3
    4
    5
    6
    7
    8
    9
    class Point3d{
    public:
    Point3d(float,float,float);
    float X() const;
    void X(float) const;
    };
    inline float Point3d::X() const {
    return x;
    }

这些程序设计风格直至今日仍然存在,虽然已经不再具备必要性。这个古老的语言规则被称为“member scope resolution rules”,大意是“一个inline函数实体,在整个class声明未被完全看见之前,是不会被evaluated的”。C++ Standard以“member scope resolution rules”来精炼这个“rewriting rule”,其效果是,如果一个inline函数在class声明后立刻被定义的话,那么还是对其evaluate,也就是说,

1
2
3
4
5
6
7
8
9
10
extern float x;
class Point3d{
public:
//对函数的分析将延迟至class声明的右大括号出现才开始
float X() const {return x;}
...
private:
float x,y,z;
};
//分析在这里进行

因此,在一个inline member function躯体内的一个data member绑定操作,会在整个class声明完成后才完成。

然而,这对于member function的argument list并不为真。Argument list中的名称还是会在它们第一次遭遇时被适当地resloved。因此在extern与nested type names之间的非直觉操作绑定操作还是会发生。例如,在下面的程序段中,length的类型在两个member function signatures中都被reslove为global typedef,也就是int。当后续再有length的nested typedef声明出现时,C++
Standard就把之前的绑定标示为非法:

1
2
3
4
5
6
7
8
9
10
11
12
13
typedef int length;
class Point3d{
public:
//length被resloved为global
//_val被resloved为Point3d::_val
void mumble(length val) {_val = val;}
length mumble() {return _val;}
private:
//length必须在“本class对它的第一个参考操作”之前被看见
//该声明将使之前的参考操作不合法
typedef float length;
length _val;
};

上述这种语言状况,仍然需要某种防御性编程风格:请始终将nested type声明放在class的起始处。在上述例子中,如果把length的nested type定义于“在class中被参考”之前,就可以确保非直觉绑定的正确性。