pointer subtract pointer?

3 views
Skip to first unread message

Dean

unread,
Dec 5, 2002, 2:25:56 PM12/5/02
to
Hi group,

I am writing a test program, my compiler is VC 6.0 on Windows XP.
My question is : why offset1 is 8, offset2 is 0?

please enlighten me. thanks.

the following is my sample code.

// ------------------------
#include <iostream>
using namespace std;

class root
{
public:
int i;
virtual int f1() { return 0; };
};

class base
{
public:
int j;
virtual int f2() { return 1; }
};

class derived : public base, public root
{
};

int main()
{
derived * pD = new derived();
root* pR;
int offset1, offset2;

pR = static_cast<root*>(pD);
offset1 = (int)pR - (int)pD;//8
offset2 = (int)(pR - pD);//0

delete pD;
return 0;
}

[ Send an empty e-mail to c++-...@netlab.cs.rpi.edu for info ]
[ about comp.lang.c++.moderated. First time posters: do this! ]

TB

unread,
Dec 5, 2002, 7:33:30 PM12/5/02
to

"Dean" <trtr...@yahoo.ca> a écrit dans le message news:
d5d99561.0212...@posting.google.com...

> Hi group,
>
> I am writing a test program, my compiler is VC 6.0 on Windows XP.
> My question is : why offset1 is 8, offset2 is 0?
>
> class root
> {
> public:
> int i;
> virtual int f1() { return 0; };
> };
>
> class base
> {
> public:
> int j;
> virtual int f2() { return 1; }
> };
>
> class derived : public base, public root
> {
> };
>
> int main()
> {
> derived * pD = new derived();
> root* pR;
> int offset1, offset2;
>
> pR = static_cast<root*>(pD);
Or even
pR = pD;
Because we're converting to a public base address.

> offset1 = (int)pR - (int)pD;//8
> offset2 = (int)(pR - pD);//0
This is a big difference between C and C++: in C++ casting between
pointer types can change the pointer's address. This is only
really the case with multiple or virtual inheritance, which is
what you have here. The idea is that the pointer, following the
conversion between types in the same hierarchy, should always
point to the object in question. But since the pointer provides a
different interpretation or vision of the object, the pointer
value (address) may change to provide this vision.

In your case, your derived object probably has a layout like
this:
base-part
root-part
derived-part
and your pointer points to the head (base) part of the structure.
So using a derived* provides the vision of both base classes and
the derived class.

Now if you convert your pointer to base*, the base vision can be
obtained by using a pointer to the base part, which just happens
here to be at the same address as the head of your derived object.

But you are converting to root*, looking for the view of your
derived object as a root object. root is unrelated to base, so you
cannot just use the same address: you have to adjust the pointer
value so that it holds the address of the root part. That way
code using the root part can use it as if it were a stand-alone
root object, and not worry about the fact that it is within (as
a base class of) another object (of class derived).

It is this adjustment your numeric subtraction of pointers reveals.

Since the pointers both point to the same object, in your pointer
subtraction, both pointers are made to be the same type, ie root*s
and so point to the same thing: the root part of the object, so
the difference there is zero.

BTW This is why you should use dynamic_cast when converting from
a root* to a derived* (where there are virtual functions present).
The C++ run-time can check that your root object is in fact a
base class part of some derived object, rather than a stand-alone
root object, or the root class part of some other object type. If
it is the root part of a derived object, the conversion

pD = dynamic_cast<derived*>(pR);

will give a non-null value - the re-adjusted address pointing to
the head of the derived object. (If you have no virtuals in the
root class, the run-time won't have the necessary information to
make this conversion.)

Any clearer?

TB

Paul Lloyd

unread,
Dec 6, 2002, 1:34:58 AM12/6/02
to
Hi Dean

The reason that offset 1 == 8:
The layout of derived in memory is:
4 bytes for base::j
4 bytes for virtual function ptr for base::f2
4 bytes for root::i
4 bytes for virtual function ptr for root::f1

(The order of the different base classes in multiple inheritance is, I
believe, not guaranteed, this is how VC6 lays them out on a 32 bit
Wintel
system).

So the start of the 'root' part of derived is at offset 8 from the start
of
the class.
The static_cast of pD therefore points to offset 8 from the start of pD
This is why (int) pR - (int) pD is 8
(Incidentally if you did base* pB=static_cast<base*>(pD) then (int) pB -
(int) pD == 0)

The reason offset2 == 0:


offset2 = (int)(pR - pD);

Is actually doing the following:
offset2 = (int)( pR - static_cast<root*>(pD));
(Examination of the disassembly confirms this: the code is identical).
But <static_cast<root*>(pD) == pR
So offset2 = (int)(pR - pR) == 0

Hope this helps

Paul

"Dean" <trtr...@yahoo.ca> wrote in message
news:d5d99561.0212...@posting.google.com...

James Kanze

unread,
Dec 6, 2002, 1:09:32 PM12/6/02
to
TB <set...@free.fr> wrote in message
news:<LeRH9.2018$Yi1...@news4.srv.hcvlny.cv.net>...
> "Dean" <trtr...@yahoo.ca> a crit dans le message news:
> d5d99561.0212...@posting.google.com...

> > I am writing a test program, my compiler is VC 6.0 on Windows XP.


> > My question is : why offset1 is 8, offset2 is 0?

> > class root
> > {
> > public:
> > int i;
> > virtual int f1() { return 0; };
> > };

> > class base
> > {
> > public:
> > int j;
> > virtual int f2() { return 1; }
> > };

> > class derived : public base, public root
> > {
> > };
> >
> > int main()
> > {
> > derived * pD = new derived();
> > root* pR;
> > int offset1, offset2;
> >
> > pR = static_cast<root*>(pD);

> Or even
> pR = pD;
> Because we're converting to a public base address.

> > offset1 = (int)pR - (int)pD;//8
> > offset2 = (int)(pR - pD);//0

> This is a big difference between C and C++: in C++ casting between
> pointer types can change the pointer's address.

It can in C as well. The big difference is that C++ has several types
of pointer casts: here, we are concerned with the difference between
static_cast and reinterpret_cast. In C, a pointer cast is always the
semantic equivalent of a reinterpret_cast. In C++, a C style pointer
cast up or down in the hierarchy is a static_cast.

The conversion of a pointer to int is always a reinterpret_cast.

The result of a reinterpret_cast is implementation defined. There is a
note, however, that the results of casting a pointer to an integral type
are intended to be unsurprising to someone who knows the architecture.
In most cases, if the integral type is large enough, I would expect to
simply see the bits carried over. This could lead to some very curious
results on PDP/10's: if a and b are char*: a > b and (int)1 < (int)b
might both be true.

In the case of Intel 32 bit systems, compiled in model small (which is
all Windows or Linux support), the addressing is linear, and there are
no strange results. In the above expressions (int)pR returns the
address of the root sub-object as an int, and (int)pD the address of the
complete object as an int. Any relationship between the two is
implementation defined (but the results here are pretty typical for 32
bit machines).

In the case of pointer comparison, the pointers must be converted to a
common type using implicit pointer conversions. And all implicit
conversions are the equivalent of a static cast. In this case, the two
possible common types are void* and root* -- the standard prefers root*
in this case. So pD is converted to a root* by means of a static_cast;
the standard requires that this point to the root sub-object of derived
object. Since the root subobject is what pR points to, the standard
requires that pR - pD == 0.

I'm not sure exactly what is disturbing the original poster. In
general, casting pointers to int and subtracting will give different
results than just subtracting the pointers. In C as in C++.

--
James Kanze mailto:jka...@caicheuvreux.com
Conseils en informatique oriente objet/
Beratung in objektorientierter Datenverarbeitung

Dean

unread,
Dec 6, 2002, 8:00:24 PM12/6/02
to
Hi Paul, thanks for your reply.

>
> The reason that offset 1 == 8:
> The layout of derived in memory is:
> 4 bytes for base::j
> 4 bytes for virtual function ptr for base::f2
> 4 bytes for root::i
> 4 bytes for virtual function ptr for root::f1

The order of memory should be :


4 bytes for virtual function ptr for base::f2
4 bytes for base::j

4 bytes for virtual function ptr for root::f1


4 bytes for root::i

the following line of code can prove it.

cout << pB << " " << &(pB->j) << " " << pR << " " << &(pR->i) << endl;

>
>
> The reason offset2 == 0:
> offset2 = (int)(pR - pD);
> Is actually doing the following:
> offset2 = (int)( pR - static_cast<root*>(pD));
> (Examination of the disassembly confirms this: the code is identical).
> But <static_cast<root*>(pD) == pR
> So offset2 = (int)(pR - pR) == 0

I like this part of explanation, now I know

offset2 = (int)(pR - pD);

equal

offset2 = (int)( pR - static_cast<root*>(pD));

Thanks again.

Best regards,

Dean

Dean

unread,
Dec 6, 2002, 8:09:59 PM12/6/02
to
Thank you, TB

Now everything is crystal clear.

Best regards,

Dean

Dean

unread,
Dec 7, 2002, 2:38:07 PM12/7/02
to
Hi all,

I got another question regarding pointer subtraction.

here is the code.

//------------------------
int i, j;
int *pi, *pj;

pi = &i;
pj = &j;

offset1 = (int)(pi - pj);
offset2 = (int)pi - (int)pj;

offset2 is 4, this is reasonable.
but why offset1 is 1?

Please help.

Best regards,

Dean

Francis Glassborow

unread,
Dec 8, 2002, 7:36:26 AM12/8/02
to
In message <d5d99561.02120...@posting.google.com>, Dean
<trtr...@yahoo.ca> writes

>Hi all,
>
>I got another question regarding pointer subtraction.
>
>here is the code.
>
>//------------------------
> int i, j;
> int *pi, *pj;
>
> pi = &i;
> pj = &j;
>
> offset1 = (int)(pi - pj);
1) that subtraction has undefined behaviour because pi and pj point into
different objects. The cast is completely irrelevant. Pointer arithmetic
is in units of the size of the object being pointed to. IOWs the
difference between pointers to adjacent ints within the same object is
1.

> offset2 = (int)pi - (int)pj;

Now you have cast each of the pointers into an int and so you are
finding the difference between the results of such a cast. I agree that
on a system where sizeof(int) is 4 that result is reasonable.


>
>offset2 is 4, this is reasonable.
>but why offset1 is 1?


Now let me ask a tough question that has just crossed my mind as I wrote
the above (I won't bore everyone with the tortuous chain of thought that
led me here)

struct X{
int i;
char c;
int j;
} x;

int main(){
int * pi = &x.i;
int * pj = &x.j;

int distance = pj-pi;
std::cout << distance;
}


Is this code well-formed? If your answer is yes, what should be the
output from the executing that program? Please justify your answer from
the Standard.


--
Francis Glassborow ACCU
64 Southfield Rd
Oxford OX4 1PA +44(0)1865 246490
All opinions are mine and do not represent those of any organisation

Maciej Sobczak

unread,
Dec 8, 2002, 1:39:34 PM12/8/02
to
Hi,

"Francis Glassborow" <francis.g...@ntlworld.com> wrote in
message news:8lzu$GEblo...@robinton.demon.co.uk...

> Now let me ask a tough question that has just crossed my mind
as I wrote
> the above (I won't bore everyone with the tortuous chain of
thought that
> led me here)
>
> struct X{
> int i;
> char c;
> int j;
> } x;
>
> int main(){
> int * pi = &x.i;
> int * pj = &x.j;
>
> int distance = pj-pi;
> std::cout << distance;
> }
>
>
> Is this code well-formed?

IMHO, no. The reasoning is the same as in your response to the
previous post and the actual wording is at the end of 5.7/6
(about subtraction of pointers):

"Unless both pointers point to elements of the same array object,
or one past the last element of the array object, the behavior is
undefined."

Interestingly, my understanding of the footnote 75 in the same
part is that the above can be substituted by well-defined:

int distance = ((char*)pj - (char*)pi) / sizeof(int);

*provided* that we can *reinterpret* POD as an array of chars.
Can we?

The first problem is that the distance computed this way can be
zero (oops...) if there is no padding in the struct and if
sizeof(int) > 1. This is also the reason why the expression in
question can trigger UB in the first place.

> If your answer is yes, what should be the
> output from the executing that program?

Even if it was well-defined, the output is unspecified (or maybe
implementation-defined?), since we do not know the amount of
padding (if any) between c and j members of the struct.

--
Maciej Sobczak
http://www.maciejsobczak.com/

Distributed lib for C, C++, Python & Tcl:
http://www.maciejsobczak.com/prog/yami/

John Potter

unread,
Dec 8, 2002, 1:39:52 PM12/8/02
to
On 8 Dec 2002 07:36:26 -0500, Francis Glassborow
<francis.g...@ntlworld.com> wrote:

> Now let me ask a tough question that has just crossed my mind as I
wrote
> the above (I won't bore everyone with the tortuous chain of thought
that
> led me here)

> struct X{
> int i;
> char c;
> int j;
> } x;

> int main(){
> int * pi = &x.i;
> int * pj = &x.j;

> int distance = pj-pi;
> std::cout << distance;
> }

> Is this code well-formed?

Yes, 5.7/2.

> If your answer is yes, what should be the output from the executing
that program?

Anything or nothing because it is undefined behavior, 5.7/7.

Note that the char has nothing to do with the answer. Subtracting
pointers to elements of the same array is defined. Subtracting
pointers to other objects of the same type including elements of the
same struct is well-formed with undefined behavior.

Here is a simple example which shows your intended confusion.

#include <iostream>
struct I2 {
int a, b;
};
struct X {
I2 i;
char c;
I2 j;
} x;
int main(){
I2 * pi = &x.i;
I2 * pj = &x.j;
int distance = pj-pi;
std::cout << distance << " " << &pj - &pi << std::endl;
std::cout << (int)pj - (int)pi << " " << sizeof(X) << std::endl;
}

The undefined behavior on my system gives:

1 -1
12 20

John

Francis Glassborow

unread,
Dec 8, 2002, 5:42:34 PM12/8/02
to
In message <asvmku$17r$1...@SunSITE.icm.edu.pl>, Maciej Sobczak
<mac...@maciejsobczak.com> writes

>Hi,
>
>"Francis Glassborow" <francis.g...@ntlworld.com> wrote in
>message news:8lzu$GEblo...@robinton.demon.co.uk...
>
>> Now let me ask a tough question that has just crossed my mind
>as I wrote
>> the above (I won't bore everyone with the tortuous chain of
>thought that
>> led me here)
>>
>> struct X{
>> int i;
>> char c;
>> int j;
>> } x;
>>
>> int main(){
>> int * pi = &x.i;
>> int * pj = &x.j;
>>
>> int distance = pj-pi;
>> std::cout << distance;
>> }
>>
>>
>> Is this code well-formed?
>
>IMHO, no. The reasoning is the same as in your response to the
>previous post and the actual wording is at the end of 5.7/6
>(about subtraction of pointers):

In all these years I had never noticed that requirement for subtraction
of pointers (which is different from that for comparison of same). That
seems to mean that the above would be undefined even if there were no
intervening char.

>
>"Unless both pointers point to elements of the same array object,
>or one past the last element of the array object, the behavior is
>undefined."
>
>Interestingly, my understanding of the footnote 75 in the same
>part is that the above can be substituted by well-defined:
>
>int distance = ((char*)pj - (char*)pi) / sizeof(int);
>
>*provided* that we can *reinterpret* POD as an array of chars.
>Can we?

Yes, I think that has to be supported for C compatibility.

>
>The first problem is that the distance computed this way can be
>zero (oops...) if there is no padding in the struct and if
>sizeof(int) > 1.

I think not, but proving that would probably be tortuous.

> This is also the reason why the expression in
>question can trigger UB in the first place.

I think this is a case where undefined behaviour would be overkill. But
using the result in a way that generated some form of pointer would be
so.

>
>> If your answer is yes, what should be the
>> output from the executing that program?
>
>Even if it was well-defined, the output is unspecified (or maybe
>implementation-defined?), since we do not know the amount of
>padding (if any) between c and j members of the struct.

Which was part of what crossed my mind late last night when I posed the
question.


--
Francis Glassborow ACCU
64 Southfield Rd
Oxford OX4 1PA +44(0)1865 246490
All opinions are mine and do not represent those of any organisation

[ Send an empty e-mail to c++-...@netlab.cs.rpi.edu for info ]

John Potter

unread,
Dec 8, 2002, 6:55:47 PM12/8/02
to
On 8 Dec 2002 13:39:34 -0500, "Maciej Sobczak"
<mac...@maciejsobczak.com> wrote:

> > Is this code well-formed?

> IMHO, no. The reasoning is the same as in your response to the
> previous post and the actual wording is at the end of 5.7/6
> (about subtraction of pointers):

> "Unless both pointers point to elements of the same array object,
> or one past the last element of the array object, the behavior is
> undefined."

Fine point. If the code is not well-formed, a diagnostic is required
and there will be no behavior to define.

ptrdiff_t ptrSub (int* lhs, int* rhs) { return lhs - rhs; }
void f () {
int x[2];
struct S { int a, b };
cout << ptrSub(x + 1, x) << endl; // 1
cout << ptrSub(x + 2, x + 1) << endl; // 1
cout << ptrSub(&a, x) << endl; // undefined
cout << ptrSub(&b, &a) << endl; // undefined
}

Everything is well-formed. If execution reaches the undefined
behavior, the implementation may use a time machine to go back to
compile time and issue a diagnostic. Otherwise, there can be no
diagnostic for this well-formed code.

John

Andy Sawyer

unread,
Dec 9, 2002, 3:17:39 PM12/9/02
to
In article <8lzu$GEblo...@robinton.demon.co.uk>,
on 8 Dec 2002 07:36:26 -0500,
Francis Glassborow <francis.g...@ntlworld.com> wrote:

> Now let me ask a tough question that has just crossed my mind as I
> wrote the above (I won't bore everyone with the tortuous chain of
> thought that led me here)
>
> struct X{
> int i;
> char c;
> int j;
> } x;
>
> int main(){
> int * pi = &x.i;
> int * pj = &x.j;
>
> int distance = pj-pi;
> std::cout << distance;
> }
>
>
> Is this code well-formed?

Yes, it is well-formed - althought I suspect that's not actually the
question you intended to ask.

> If your answer is yes, what should be the output from the executing
> that program?

Undefined behaviour, which is why I suspect that you weren't really
asking about well-formed-ness.

> Please justify your answer from the Standard.

5.7p6 defines the behaviour of operator- for pointer types. Since the
pointers in your example do not satisfy the requirements therein, the
behaviour is undefined.

Regards,
Andy S.
--
"Light thinks it travels faster than anything but it is wrong. No matter
how fast light travels it finds the darkness has always got there
first,
and is waiting for it." -- Terry Pratchett, Reaper Man

James Kanze

unread,
Dec 9, 2002, 4:30:04 PM12/9/02
to
jpo...@falcon.lhup.edu (John Potter) wrote in message
news:<3df3965b...@news.earthlink.net>...

> On 8 Dec 2002 13:39:34 -0500, "Maciej Sobczak"
> <mac...@maciejsobczak.com> wrote:

> > > Is this code well-formed?

> > IMHO, no. The reasoning is the same as in your response to the
> > previous post and the actual wording is at the end of 5.7/6 (about
> > subtraction of pointers):

> > "Unless both pointers point to elements of the same array object, or
> > one past the last element of the array object, the behavior is
> > undefined."

> Fine point. If the code is not well-formed, a diagnostic is required
> and there will be no behavior to define.

> ptrdiff_t ptrSub (int* lhs, int* rhs) { return lhs - rhs; }
> void f () {
> int x[2];
> struct S { int a, b };
> cout << ptrSub(x + 1, x) << endl; // 1
> cout << ptrSub(x + 2, x + 1) << endl; // 1
> cout << ptrSub(&a, x) << endl; // undefined
> cout << ptrSub(&b, &a) << endl; // undefined
> }

> Everything is well-formed. If execution reaches the undefined
> behavior, the implementation may use a time machine to go back to
> compile time and issue a diagnostic. Otherwise, there can be no
> diagnostic for this well-formed code.

No time machine is required. A compiler is allowed to issue a
diagnostic any time it wishes.

If the compiler can determine that f will be called, of course, it can
also refuse to compile the code. Even if it cannot determine this, it
can generate anything it wishes for f. (A call to abort() might be a
good choice. A call to the system function to reformat the hard disk is
also legal from a standards point of view, but is unlikely to result in
a great commercial success.)

And of course, none of this has changed since the early days of C.

--
James Kanze mailto:jka...@caicheuvreux.com

Conseils en informatique orientée objet/
Beratung in objektorientierter Datenverarbeitung

[ Send an empty e-mail to c++-...@netlab.cs.rpi.edu for info ]

John Potter

unread,
Dec 10, 2002, 2:33:34 PM12/10/02
to
On 9 Dec 2002 16:30:04 -0500, ka...@gabi-soft.de (James Kanze) wrote:

> jpo...@falcon.lhup.edu (John Potter) wrote in message
> news:<3df3965b...@news.earthlink.net>...

> > Fine point. If the code is not well-formed, a diagnostic is


required
> > and there will be no behavior to define.

> > ptrdiff_t ptrSub (int* lhs, int* rhs) { return lhs - rhs; }
> > void f () {
> > int x[2];
> > struct S { int a, b };
> > cout << ptrSub(x + 1, x) << endl; // 1
> > cout << ptrSub(x + 2, x + 1) << endl; // 1
> > cout << ptrSub(&a, x) << endl; // undefined
> > cout << ptrSub(&b, &a) << endl; // undefined
> > }

> > Everything is well-formed. If execution reaches the undefined
> > behavior, the implementation may use a time machine to go back to
> > compile time and issue a diagnostic. Otherwise, there can be no
> > diagnostic for this well-formed code.

> No time machine is required. A compiler is allowed to issue a
> diagnostic any time it wishes.

Yes, I expected someone, likely you, would catch this one. Rather
than diagnostic, it should be refusal to produce an executable.

> If the compiler can determine that f will be called, of course, it can
> also refuse to compile the code. Even if it cannot determine this, it
> can generate anything it wishes for f. (A call to abort() might be a
> good choice. A call to the system function to reformat the hard disk
is
> also legal from a standards point of view, but is unlikely to result
in
> a great commercial success.)

It needs to determine that f will be called and that calling ptrSub
will subtract the pointers. A bit more than most implementations can
determine with ptrSub in a different translation unit.

> And of course, none of this has changed since the early days of C.

Sure, the code is well-formed. In the absense of a time machine, the
implementation will usually just produce possibly expected output.
After all, it is just integer arithmetic.

void cp1 (void* bi, void* ei, void* ri) {
unsigned char* b(static_cast<unsigned char*>(bi));
unsigned char* e(static_cast<unsigned char*>(ei));
unsigned char* r(static_cast<unsigned char*>(ri));
for (; b != e; ++ b, ++ r)
*r = *b;
}
void cp2 (void* bi, void* ei, void* ri) {
unsigned char* b(static_cast<unsigned char*>(bi));
int n(static_cast<unsigned char*>(ei) - b); // pointer subtraction
unsigned char* r(static_cast<unsigned char*>(ri));
for (int x = 0; x != n; ++ x)
r[x] = b[x];
}
struct S {
char a, b;
};
void f () {
S s1, s2;
cp1(&s1, &s1 + 1, &s2); // well defined
cp2(&s1, &s1 + 1, &s2); // well defined? s1 is s1[1]
cp1(&s1.a, &s1.b + 1, &s2.a); // well defined
cp2(&s1.a, &s1.b + 1, &s2.a); // undefined
}

Let's not try to make sense of this nonsense. :)

John

Dean

unread,
Dec 13, 2002, 3:22:13 PM12/13/02
to
Hi Andy,

>
> 5.7p6 defines the behaviour of operator- for pointer types. Since the
> pointers in your example do not satisfy the requirements therein, the
> behaviour is undefined.
>
> Regards,
> Andy S.

What does 5.7p6 mean?
It seems you guys are talking about a book, kind of bible. Can you
tell me the name of it?

{Its the C++ Standard, which for C++ programmers is pretty close to the
'Bible' -mod/fwg}

Thanks.

Dean

llewelly

unread,
Dec 15, 2002, 7:24:44 AM12/15/02
to
trtr...@yahoo.ca (Dean) writes:

> Hi Andy,
>
> >
> > 5.7p6 defines the behaviour of operator- for pointer types. Since the
> > pointers in your example do not satisfy the requirements therein, the
> > behaviour is undefined.
> >
> > Regards,
> > Andy S.
>
> What does 5.7p6 mean?

Section 5.7, paragraph 6. Alternatively, The Book of Expressions,
chapter 7, verse 6.

> It seems you guys are talking about a book, kind of bible. Can you
> tell me the name of it?
>
> {Its the C++ Standard, which for C++ programmers is pretty close to the
> 'Bible' -mod/fwg}

[snip]

which can be had for $18 at
http://webstore.ansi.org/ansidocstore/product.asp?sku=INCITS%2FISO%2FIEC+14882%2D1998

or an alternate store appropriate to your nation.

Reply all
Reply to author
Forward
0 new messages