爱技术 & 爱分享
爱蛋蛋 & 爱生活

Data Member 绑定

这篇文章主要是跟大家聊一下,C++ Data Member 绑定的历史故事,以及对我们现在的代码产生的影响。

extern float x;

class Point3D
{
public:
    Point3D(float,float,float);
    //问题,被传回和被设定的x 是哪一个 x呢?
    float X() const {return x;}
    void X(float _newx)const {x=_newx;}
    //...
private:
    float x,y,z;
};

此段代码中的那个问题,我相信现在大家都能回答的出来吧。 被X()返回的x 毋庸置疑 就是 Point3D内部的那个 x。

不过在早期的 C++编译器上,因为编译器是先见到 外部的x,然后再见到Get 和 Set函数,所以x会被理解成 外部的x,毕竟在看到Point3D内部的x之前,函数已经被解析完成了。

所以当时有一种防御性编程风格:

1 . 所有的data member都放在 class 的首部,以确保正确的绑定:

class Point3D
{
    //防御性编程风格
    //在class 的首部声明出所有的data member
    float x,y,z;
public:
    float X() const{return  x;}
    //....
};

2 . 所有的inline functions,不管大小都放在 class 声明之外:

class Point3D
{
    //防御性编程风格
    //把所有的 inlines 都移到 class 之外
public:
    Point3D();
    float X()const;
    void X(float _newx)const;
};


inline float Point3D::X()const
{
    return x;
}
//...

虽然我们现在可能还能看到这样的风格,不过第2点自从C++2.0之后就没必要了。这个古老的规则被称为”member rewriting rule”。

一个inline函数的函数体,在整个class的声明未被完全看见之前,是不会被解析的。在C++ Standard 中的称呼为 “member scope resolution rules”

所以现代C++对于 inline member function body 的解析会直到整个class 声明出现了才开始,所以在见到class 的右大括号}之前,不会解析函数体。

不过这对于 member function 的 argument list 并不为真。argument list 中的名称还是会在他们第一次遇到时被解析。因此extern 和 nested type names 之间的非直觉绑定操作还是会发生。

所以下面这段程序中的set 函数的 length 类型最开始都会被解析为 long long 然后当解析到类中定义的 length的时候,才会把之前的解析结果给废除,重新解析。

typedef  long long length;

class test
{
public:

    void SetVal1(length _val) { printf("%d\t%d\n", sizeof(length), sizeof(val1)); val1 = _val; }
    length GetVal1() { printf("%d\t%d\n", sizeof(length), sizeof(val1)); return val1; }

    void SetVal2(length _val) { printf("%d\t%d\n", sizeof(length),sizeof(val2));  val2 = _val; }
    length GetVal2() { printf("%d\t%d\n", sizeof(length), sizeof(val2)); return val2; }

private:
    length val1;
    typedef  int length;
    length val2;
};
int main()
{
    test t;
    t.SetVal1(0);
    t.GetVal1();
    printf("%d\n", sizeof(t.GetVal1()));
    t.SetVal2(0);
    t.GetVal2();
    printf("%d\n", sizeof(t.GetVal2()));
    return 0;
}

看了上面的讲述,大家对于函数中的sizeof(length)的结果都很清楚了吧,等价于sizeof(int)

不过如果你运行上面的代码的话,你会发现sizeof(val1);sizeof(val2)是不一样的,在遇到类内部定义的length之后,val1的类型并未被更改成int,还是之前的long long。不过这是在Visual Studio 2017上的结果,在gcc 上不予通过编译。

以下为Visual Studio上的运行结果

4   8
4   8
8
4   4
4   4
8

但是这里有一点值得注意的是,如果将上述代码修改为下面这样:

typedef  long long length;

class test
{
public:

    void SetVal1(length _val);
    length GetVal1();

    void SetVal2(length _val);
    length GetVal2();

private:
    length val1;
    typedef  int length;
    length val2;
};

void test::SetVal1(length _val)
{
    printf("%d\t%d\n", sizeof(length), sizeof(val1));
    val1 = _val;
}

length test::GetVal1()
{
    printf("%d\t%d\n", sizeof(length), sizeof(val1));
    return val1;
}

void test::SetVal2(length _val)
{
    printf("%d\t%d\n", sizeof(length),sizeof(val2));
    val2 = _val;
}

length test::GetVal2()
{
    printf("%d\t%d\n", sizeof(length), sizeof(val2)); 
    return val2;
}

int main()
{
    test t;
    t.SetVal1(0);
    t.GetVal1();
    printf("%d\n", sizeof(t.GetVal1()));
    t.SetVal2(0);
    t.GetVal2();
    printf("%d\n", sizeof(t.GetVal2()));
    return 0;
}

编译是无法通过的,因为声明与其定义中的 length 含义是不一致的,所以会编译会报声明与定义不匹配的错误。

所以上述的情况,还是需要防御性编程:请总是把”nested type声明”放在 class的起始处。在上述代码中,如果把 length 的nested定义放在 val1被声明之前,就可以确保绑定的正确性。

赞(0) 传送门
本作品采用知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议进行许可。编程日志 » Data Member 绑定
分享到: 更多 (0)

游戏 && 后端

传送门传送门