tags:

views:

598

answers:

9

I was wondering why the following code doesn't compile:

void foo_int(int *a) { }
void foo_long(long *a) { }

int main()
{
    int i;
    long l;

    foo_long(&i);
    foo_int(&l);
}

I am using GCC, and neither calls work either in C or C++. Since it is a 32-bit system, both int and long are signed 32-bit integers (which can be verified with sizeof at compile time).

The reason I am asking is that I have two separate header files, neither are under my control, and one does something like: typedef unsigned long u32; and the other: typedef unsigned int uint32_t;. The declarations are basically compatible, except when I use them as pointers as in the above code snippet, I have to explicitly cast.

Any idea why this is?

+26  A: 

...because the C++ standard defines int and long to be two distinct types regardless of their value range, representation, etc. If by "basically compatible" you mean convertible to each other, then yes.

sellibitze
+7  A: 

long and int are two distinct types, even if they are the same size. There would be huge consequences in the c++ template world if they were treated the same by some compilers.

rlbond
+23  A: 

Just because long and int both happen to be 32-bit on your particular compiler and hardware, does not mean that they will always both be 32-bit on every kind of hardware and every compiler.

C (and C++) were designed to be source-portable between different compilers and different hardware.

system PAUSE
+15  A: 

I can see two distinct questions here.

First, on modern architectures it's pretty safe to assume that pointers are the same size (that is, there are no near/far pointers; but pointers to member functions aren't regular pointers and may be a different size); and on a 32 bit system that size is generally 32 bits. C even goes so far as to automatically cast void* to anything else because, after all, a pointer is really just a memory address. However the language definitions distinguish various pointers as different types. I believe the reason for this is that different types can have different alignment (the void* rule is that nothing can really be of type void, so if you have a pointer to void you likely know the correct type and, implicitly, the correct alignment(see note))

Second, as others have pointed out, longs and ints are fundamentally different types with default conversions built in. The standards require longs to be at least as large as ints, but possibly larger. On your architecture, the alignment is probably the same but on other architectures that could be different as well.

It is possible to fudge in your case, but it's not portable. If you do not #include the correct function declarations, and instead simply forward declare them yourself things should magically work because in your case longs and ints are compatible (assuming no signedness issues; also in C++ you'll need to extern "C" both your declarations and the actual function implementations so that you don't get link errors). Until you switch to a different compiler, or different operating system, or different architecture, etc.

For instance, in C++ you could do this:

// in file lib.cc
#include <iostream>

extern "C" void foo_int(int* a)
{
    std::cout << "foo_int " << *a << " at address " << a <<'\n';
}

extern "C" void foo_long(long* a)
{
    std::cout << "foo_long " << *a << " at address " << a <<'\n';
}


// In file main.cc
extern "C" void foo_int(long* a);
extern "C" void foo_long(int* a);

int main()
{
    int i = 5;
    long l = 10;

    foo_long(&i);
    foo_int(&l);
}

(In C, you would get rid of the extern "C" and use printf instead of cout).

Using GCC you would compile like so:

$ g++ -c lib.cc -o lib.o
$ g++ main.cc lib.o
$ ./a.out
foo_long 5 at address 0x22cce4
foo_int 10 at address 0x22cce0


NOTE Since there are no objects of type void, a void* can only point to objects of some other type. And the compiler knew that real type when it put the object there. And the compiler knew the alignment for that type when it allocated the object. You may not really know the alignment, but the cast is only guaranteed to work if it's back to the original type, in which case the alignment and size will be the same.

But there are wrinkles. For one, the object's packing must be the same in both places (not an issue with primitive types). For another, it is possible to point at arbitrary memory with void*s, but programmers who do that presumably realize what they're doing.

Max Lybbert
this should be the selected answer.
San Jacinto
+1 for mentioning alignment.
Earlz
It's not safe to assume that pointers to members (in C++) are the same size as other pointers.
bk1e
@bk1e: Good point. I've edited the answer.
Max Lybbert
A: 

This is because on some platforms long and int are of different size.

16 bit:
long=32bits
int=16bits

32bit:
long=32bits
int=32bits

64bit(ILP64):
long=64bits
int=64bits

64bit(LP64):
long=64bits
int=32bits

64bit(LLP64): (what windows uses for whatever reason)
long long=64bits
long=32bits
int=32bits

Also, the more confusing thing is that though you must cast to interact between the two types, but you can not do function overloading like this as if they truly were two separate types

long foo(int bar);
int foo(int bar);
Earlz
That's not a long/int thing - you can never overload on return type. The compiler might treat foo(int bar) and foo(long bar) as the same which seems wrong but useful if you imagine calling foo(1);
Martin Beckett
Integer conversion is not what the question is about. It is about pointer conversion. Implicit conversion between integer types is allowed in any case.
Clifford
+1  A: 

int* and long* are distinct types, which are not necessarily the same. In every real implementation I think they are, but that's neither here nor there for a standards-conformant compiler.

I believe it was one of the early PDP machines in which a char* was larger than an int*. The reason for this was the odd size of ints on that architecture (36bits). So the system would pack multiple 9bit chars into a single int, so a char* contained the address in the format of (int*,offset inside the int). **

The standard specifies that all pointers are representable as a void*, and insinuates that char* must be the same as void*, but there is no particular requirement for the other pointer types to be convertible.

** I'm unable to find references to this, so the source of this might have been a theoretical (but still valid) example rather than an actual implementation.C++ FAQ Lite

jkerian
Every real implementation? Consider the 8-bit and 16-bit embedded world. Often, int is 16 bits, long is 32 bits.
Craig McQueen
I'm not talking about int and long, I'm talking about int* and long*. I am unaware of any implementation in which these POINTER types are not the same size. The original poster's observations that int and long are the same size on his platform is a complete red herring in this case.
jkerian
+3  A: 

Since I don't particularly like any of the answers given so far, I went to the C++ standard:

4.7 Integral conversions [conv.integral]

1 An rvalue of an integer type can be converted to an rvalue of another integer type. An rvalue of an enumeration type can be converted to an rvalue of an integer type.

This says it is allowed to implicitly convert one integer to another, so the two types (as they are the same size), are interchangeable as rvalues.

4.10 Pointer conversions [conv.ptr]

1 An integral constant expression (expr.const) rvalue of integer type that evaluates to zero (called a null pointer constant) can be converted to a pointer type. The result is a value (called the null pointer value of that type) distinguishable from every pointer to an object or function. Two null pointer values of the same type shall compare equal. The conversion of a null pointer constant to a pointer to cv-qualified type is a single conversion, and not the sequence of a pointer conversion followed by a qualification conversion (conv.qual).

2 An rvalue of type "pointer to cv T," where T is an object type, can be converted to an rvalue of type "pointer to cv void." The result of converting a "pointer to cv T" to a "pointer to cv void" points to the start of the storage location where the object of type T resides, as if the object is a most derived object (intro.object) of type T (that is, not a base class subobject).

3 An rvalue of type "pointer to cv D," where D is a class type, can be converted to an rvalue of type "pointer to cv B," where B is a base class (class.derived) of D. If B is an inaccessible (class.access) or ambiguous (class.member.lookup) base class of D, a program that necessitates this conversion is ill-formed. The result of the conversion is a pointer to the base class sub-object of the derived class object. The null pointer value is converted to the null pointer value of the destination type.

It is only allowed to implicitly convert:

  • 0 to any pointer type (making it a null pointer)
  • any pointer type to void* (properly cv-qualified)
  • derived pointer to a base pointer (properly cv-qualified)

So even though the underlying machine type is the same, it is not allowed to implicitly convert between the two types.

Greg Rogers
+1  A: 

int and long are defined to be distinct types so you can program portable.

codymanix
+1 for noting portability. You may replace "program" with "overload".
sellibitze
A: 
sambowry
I hope you realize that this is not portable. It's equivalent to a reinterpret_cast. In case int and long don't share the same "underlying type" you'll get weird results on big endian machines.
sellibitze