Reputation: 36459
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?
Upvotes: 8
Views: 3070
Reputation:
I don't think any of these answers cut to the chase.
The answer is this: valid conversion between types doesn't imply valid conversion between pointers. This makes sense, right? You want the following code to compile
char a = 12;
int b = a;
But letting this code compile would be a recipe for disaster:
void foo(int* x) { x = 0x7f8f9faf; }
// ...
char a = 12;
foo(&a);
So just because there exists a conversion between long
and int
doesn't mean that there should be a pointer conversion. But, you may protest, long
and int
have exactly the same representation on your compiler! And you'd be right but that doesn't change the fact that as far as the standard and compiler are concerned they are different types. And you can't implicitly convert between pointers to types unless there is an inheritance relationship.
Also, more generally, whilst C++ may change in whether it is valid on not based on the local definitions of how big int
, etc. are, it does not change whether it is syntactically correct. Completely collapsing the distinction between long
and int
will do this.
Upvotes: 1
Reputation: 20047
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, long
s and ints
are fundamentally different types with default conversions built in. The standards require long
s to be at least as large as int
s, 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 long
s and int
s 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.
Upvotes: 17
Reputation: 2476
You don't need to write the explicit cast by hand on every call. A simple macro can do it:
#include <stdio.h>
void foo_int( int *a ){ printf("a: %i\n", *a ); }
#define foo_int(a) foo_int( (int*)a )
int main(){
long l = 12;
foo_int( &l ); // no warning, runs as expected
foo_int( l ); // no warning, segmentation fault
}
Upvotes: 0
Reputation: 29520
int and long are defined to be distinct types so you can program portable.
Upvotes: 1
Reputation: 36459
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:
So even though the underlying machine type is the same, it is not allowed to implicitly convert between the two types.
Upvotes: 4
Reputation: 17046
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
Upvotes: 2
Reputation: 63905
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);
Upvotes: 2
Reputation: 38550
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.
Upvotes: 27
Reputation: 67839
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.
Upvotes: 7
Reputation: 28107
...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.
Upvotes: 26