File: sizes.yo

package info (click to toggle)
c%2B%2B-annotations 11.5.0-1
  • links: PTS, VCS
  • area: main
  • in suites: bullseye
  • size: 11,244 kB
  • sloc: cpp: 21,698; makefile: 1,505; ansic: 165; sh: 121; perl: 90
file content (114 lines) | stat: -rw-r--r-- 4,751 bytes parent folder | download | duplicates (3)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
    hi(size: pointer to member)hi(pointer: to member: size) An interesting
characteristic of pointers to members is that their sizes differ from those of
`normal' pointers. Consider the following little program:
        verbinclude(-a examples/size.cc)
    On a 32-bit architecture a pointer to a member function requires eight
bytes, whereas other kind of pointers require four bytes (Using GNU's g++
compiler).

    Pointer sizes are hardly ever explicitly used, but their sizes may cause
confusion in statements like:
        verb(    printf("%p", &X::fun);)

Of course, tt(printf) is likely not the right tool for displaying the
value of these bf(C++) specific pointers. The values of these pointers can be
inserted into streams when a tt(union), reinterpreting the 8-byte pointers as
a series of size_t tt(char) values, is used:
        verbinclude(-a examples/union.cc)

But why are their sizes different from the sizes of ordinary pointers? To
answer this question let's first have a look at the familiar
tt(std::fstream). It is derived from tt(std::ifstream) and
tt(std::ofstream). An tt(fstream), therefore, contains both an tt(ifstream) and
an tt(ofstream). An tt(fstream) will be organized as shown in figure
ref(PMSIZES). 

        figure(pointermembers/sizes)
            (std::fstream object organization)
            (PMSIZES)

In tt(fstream (a)) the first base class was tt(std::istream), and the second
baseclass was tt(std::ofstream). But it could also very well be the other way
around, as illustrated in tt(fstream (b)): first the tt(std::ofstream), then
the tt(std::ifstream). And that's the crux of the biscuit.

If we have an tt(fstream fstr{"myfile"}) object and do tt(fstr.seekg(0)), then
we call tt(ifstream's seekg) function. But if we do tt(fstr.seekp(0)), then we
call tt(ofstream's seekp) function. These functions have their own addresses,
say &seekg and &seekp. But when we call a member function (like
tt(fstr.seekp(0))) then what we in fact are doing is tt(seekp(&fstr, 0)).

But the problem here is that tt(&fstr) does not represent the correct object
address: tt(seekp) operates on an tt(ofstream), and that object does not start
at tt(&fstr), so (in tt(fstream (a))), at tt(&(fstr + sizeof(ifstream))).

So, the compiler, when calling a member function of a class using inheritance,
must make a correction for the relative location of an object whose members we
are calling.

However, when we're defining something like
        verb(    ostream &(fstream::*ptr)(ios::off_type step, ios::seekdir org) = &seekp;)

and then do tt((fstr->*)ptr(0)) the compiler doesn't know anymore which
function is actually being called: it merely receives the function's
address. To solve the compiler's problem the shift (for the location of the
ofstream object) is now stored in the member pointer itself. That's one reason
why the extra data field is needed when using function pointers.

Here is a concrete illustration: first we define 2 structs, each having a
member function (all inline, using single line implementations to save some
space):
        verb(    struct A
    {
        int a;
    };

    struct B
    {
        int b;
        void bfun() {}
    };)

Then we define C, which is derived from both A (first) and B (next)
(comparable to tt(fstream), which embeds tt(ifstream) and tt(ofstream)):
        verb(    struct C: public A, public B
    {};)

Next, in tt(main) we define objects of two different unions and assign the
address of tt(B::bfun) to their tt(ptr) fields, but tt(BPTR.ptr) looks at it
as a member in the tt(struct B) world, while tt(CPTR.ptr) looks at it as a
member in the tt(struct C) world.

Once the unions' pointer fields have been assigned their value[] arrays are
used to display the content of the ptr fields (see below):
        verb(    int main()
    {
        union BPTR
        {
            void (B::*ptr)();
            unsigned long value[2];
        };
        BPTR bp;
        bp.ptr = &B::bfun;
        cout << hex << bp.value[0] << ' ' << bp.value[1] << dec << '\n';

        union CPTR
        {
            void (C::*ptr)();
            unsigned long value[2];
        };
        CPTR cp;
        cp.ptr = &C::bfun;
        cout << hex << cp.value[0] << ' ' << cp.value[1] << dec << '\n';
    })

When this program is run, we see
        verb(    400b0c 0
    400b0c 4)

(your address values (the first ones on the two lines) may differ). Note
that the functions' addresses are the same, but since in the C world the B
object lives beyond the A object, and the A object is 4 bytes large, we must
add 4 to the value of the `tt(this)' pointer when calling the function from a C
object. That's exactly what the shift value in the pointer's second field is
telling the compiler.