4

I have a structure, defined by as follows:

struct vector
{
  (TYPE) *items;
  size_t nitems;
};

where type may literally be any type, and I have a type-agnostic structure of similar kind:

struct _vector_generic
{
  void *items;
  size_t nitems;
};

The second structure is used to pass structures of the first kind of any type to a resizing function, for example like this:

struct vector v;
vector_resize((_vector_generic*)&v, sizeof(*(v->items)), v->nitems + 1);

where vector_resize attempts to realloc memory for the given number of items in the vector.

int
vector_resize (struct _vector_generic *v, size_t item_size, size_t length)
{
  void *new = realloc(v->items, item_size * length);
  if (!new)
    return -1;

  v->items = new;
  v->nitems = length;

  return 0;
}

However, the C standard states that pointers to different types are not required to be of the same size.

6.2.5.27:

A pointer to void shall have the same representation and alignment requirements as a pointer to a character type.39) Similarly, pointers to qualified or unqualified versions of compatible types shall have the same representation and alignment requirements. All pointers to structure types shall have the same representation and alignment requirements as each other. All pointers to union types shall have the same representation and alignment requirements as each other. Pointers to other types need not have the same representation or alignment requirements.

Now my question is, should I be worried that this code may break on some architectures?

Can I fix this by reordering my structs such that the pointer type is at the end? for example:

struct vector
{
  size_t nitems;
  (TYPE) *items;
};

And if not, what can I do?

For reference of what I am trying to achieve, see:
https://github.com/andy-graprof/grapes/blob/master/grapes/vector.h

For example usage, see:
https://github.com/andy-graprof/grapes/blob/master/tests/grapes.tests/vector.exp

Andreas Grapentin
  • 5,499
  • 4
  • 39
  • 57
  • @Joey the interpretation and the quote are from this answer: http://stackoverflow.com/a/1241314/885605 – Andreas Grapentin Nov 25 '14 at 07:03
  • I believe you may be thinking in terms of **apples** and **oranges**. The reference to `pointer size` means that on some architectures the **size of a pointer** may be different (e.g. `4-bit` vs. `8-bit`, etc..). This has nothing to do with what the **pointer points to**. I may not understand completely the crux of your question, but it this is where the hang-up is, you may be thinking about it sideways. – David C. Rankin Nov 25 '14 at 07:20
  • @DavidC.Rankin I am indeed thinking of the size of the actual pointer, not what it points to. My issue is that if the size of (TYPE*) is not equal to the size of (void*) then my code will break. – Andreas Grapentin Nov 25 '14 at 07:57
  • 1
    Well then I apologize, and must admit difficulty in envisioning a situation where a pointer of (TYPE*) could not be cast to (void*) so as to provide a more productive comment. The only issue I saw from a pointer size perspective would be some inane addressing scheme where a smaller pointer size would prevent addressing the target on different hardware (e.g. some hand-rolled memory manager, etc.). Sorry for the noise. – David C. Rankin Nov 25 '14 at 08:05
  • @DavidC.Rankin Yes, casting the pointer directly would work, but I instead cast a pointer to a struct containing the pointer, where the structs are incompatible. But your comment is very much valid, maybe I should cast the members directly instead of using a generic intermediate struct. – Andreas Grapentin Nov 25 '14 at 08:22

3 Answers3

2

You code is undefined.

Accessing an object using an lvalue of an incompatible type results in undefined behavior.

Standard defines this in:

6.5 p7:

An object shall have its stored value accessed only by an lvalue expression that has one of the following types:

— a type compatible with the effective type of the object,

— a qualified version of a type compatible with the effective type of the object,

— a type that is the signed or unsigned type corresponding to the effective type of the object,

— a type that is the signed or unsigned type corresponding to a qualified version of the effective type of the object,

— an aggregate or union type that includes one of the aforementioned types among its members (including, recursively, a member of a subaggregate or contained union), or

— a character type.

struct vector and struct _vector_generic have incompatible types and do not fit into any of the above categories. Their internal representation is irrelevant in this case.

For example:

struct vector v;
_vector_generic* g = &v;
g->size = 123 ;   //undefined!

The same goes for you example where you pass the address of the struct vector to the function and interpret it as a _vector_generic pointer.

The sizes and padding of the structs could also be different causing elements to be positioned at different offsets.

What you can do is use your generic struct, and cast if depending on the type the void pointer holds in the main code.

struct gen
{
    void *items;
    size_t nitems;
    size_t nsize ;
};

struct gen* g = malloc( sizeof(*g) ) ;
g->nitems = 10 ;
g->nsize = sizeof( float ) ;
g->items = malloc( g->nsize * g->nitems ) ;
float* f = g->items ;
f[g->nitems-1] = 1.2345f ;
...

Using the same struct definition you can allocate for a different type:

struct gen* g = malloc( sizeof(*g) ) ;
g->nitems = 10 ;
g->nsize = sizeof( int ) ;
g->items = malloc( g->nsize * g->nitems ) ;
int* i = g->items ;
...

Since you are storing the size of the type and the number of elements, it is obvious how your resize function would look like( try it ).

You will have to be careful to remember what type is used in which variable as the compiler will not warn you because you are using void*.

2501
  • 25,460
  • 4
  • 47
  • 87
1

The code in your question invokes undefined behaviour (UB), because you de-reference a potentially invalid pointer. The cast:

(_vector_generic*)&v

... is covered by 6.3.2.3 paragraph 7:

A pointer to an object type may be converted to a pointer to a different object type. If the resulting pointer is not correctly aligned for the referenced type, the behavior is undefined. Otherwise, when converted back again, the result shall compare equal to the original pointer.

If we assume alignment requirements are met, then the cast does not invoke UB. However, there is no requirement that the converted pointer must "compare equal" with (i.e. point at the same object as) the original pointer, nor even that it points to any object at all - that is to say, the value of the pointer is unspecified - therefore, to dereference this pointer (without first ascertaining that it is equal to the original) invokes undefined behaviour.

(Many people who know C well find this odd. I think this is because they know a pointer cast usually compiles to no operation - the pointer value simply remains as it is - and therefore they see pointer conversion as purely a type conversion. However, the standard does not mandate this).

Even if the pointer after conversion did compare equal with the original pointer, 6.5 paragraph 7 (the so-called "strict aliasing rule") would not allow you to dereference it. Essentially, you cannot access the same object via two pointers with different type, with some limited exceptions.

Example:

struct a { int n; };
struct b { int member; };

struct a a_object;
struct b * bp = (struct b *) &a_object; // bp takes an unspecified value

// Following would invoke UB, because bp may be an invalid pointer:
// int m = b->member;

// But what if we can ascertain that bp points at the original object?:
if (bp == &a_object) {
    // The comparison in the line above actually violates constraints
    // in 6.5.9p2, but it is accepted by many compilers.
    int m = b->member;   // UB if executed, due to 6.5p7.
}
davmac
  • 20,150
  • 1
  • 40
  • 68
  • *However, there is no requirement ( omitted ) that it points to any object at all* No true, because: *6.3.2.3, p7: A pointer to an object type may be converted to a pointer to a different object type. If the resulting pointer is not correctly aligned for the referenced type, the behavior is undefined. Otherwise, when converted back again, the result shall compare equal to the original pointer.* and *6.2.5, p 28: All pointers to structure types shall have the same representation and alignment requirements as each other.* So you can convert between two structs as long as you don't dereference. – 2501 Apr 27 '15 at 14:18
  • @2501 "6.2.5, p 28: All pointers to structure types shall have the same representation and alignment requirements as each other" - This has no bearing on the result of pointer conversion. "So you can convert between two structs as long as you don't dereference" - my answer already says that you can convert between two pointer types as long as you don't dereference. – davmac Apr 27 '15 at 14:21
  • *Otherwise, when converted back again, the result shall compare equal to the original pointer.* So you can assign to a pointer of a different struct type, and back, which makes the pointer assignment defined( but of course not the dereference ). – 2501 Apr 27 '15 at 14:23
  • @2501 The assignment back to the original type is well defined. The in-between pointer value is unspecified. (Making this an example of _unspecified behavior_ but not _undefined behavior_). To then dereference the unspecified pointer is UB. – davmac Apr 27 '15 at 14:24
  • @2501 very well, though I feel comments can be of value and I would much prefer if you would leave your comments on my answer rather than also deleting them. – davmac Apr 27 '15 at 14:55
0

Lets for the sake of discussion ignore that the C standard formally says this is undefined behavior. Because undefined behavior simply means that something is beyond the scope of the language standard: anything can happen and the C standard makes no guarantees. There may however be "external" guarantees on the particular system you are using, made by those who made the system.

And in the real world where there is hardware, there are indeed such guarantees. There are just two things that can go wrong here in practice:

  • TYPE* having a different representation or size than void*.
  • Different struct padding in each struct type because of alignment requirements.

Both of these seem unlikely and can be dodged with a static asserts:

static void ct_assert (void) // dummy function never linked or called by anyone
{
  struct vector v1;
  struct _vector_generic v2;

  static_assert(sizeof(v1.items) == sizeof(v2.items), 
                "Err: unexpected pointer format.");
  static_assert(sizeof(v1) == sizeof(v2), 
                "Err: unexpected padding.");
}

Now the only thing left that could go wrong is if a "pointer to x" has same size but different representation compared to "pointer to y" on your specific system. I have never heard of such a system anywhere in the real world. But of course, there are no guarantees: such obscure, unorthodox systems may exist. In that case, it is up to you whether you want to support them, or if it will suffice to just have portability to 99.99% of all existing computers in the world.

In practice, the only time you have more than one pointer format on a system is when you are addressing memory beyond the CPU's standard address width, which is typically handled by non-standard extensions such as far pointers. In all such cases, the pointers will have different sizes and you will detect such cases with static assert above.

Lundin
  • 195,001
  • 40
  • 254
  • 396
  • 1
    Strict aliasing is very real with modern compilers and you break it. Static asserts don't help with that. – 2501 Nov 25 '14 at 08:23
  • static assertions are a valuable hint. I will make sure to remember that. – Andreas Grapentin Nov 25 '14 at 08:23
  • @2501 Give one example from the real world where it would cause problems, given that variable sizes and alignment are in check with asserts? – Lundin Nov 25 '14 at 08:30
  • @2501 Yes, I am perfectly aware. Breaking it is undefined behavior, something beyond the scope of the C standard. But that doesn't really matter if your implementation makes guarantees beyond the C standard. It is easy enough to say that something is UB, but just because it is, it doesn't mean that it is an actual problem in the real world, where programs run on CPUs and where all hardware is perfectly well-defined. – Lundin Nov 25 '14 at 08:38
  • 1
    Depends how you define *a problem in the real world*: http://stackoverflow.com/questions/2958633/gcc-strict-aliasing-and-horror-stories https://www.google.com/search?hl=en&safe=off&q=strict+aliasing+bug http://davmac.wordpress.com/2009/10/ In my opinion suggesting code that violates the standard is irresponsible. – 2501 Nov 25 '14 at 08:44
  • @2501 Writing code that relies on UB doesn't violate the C standard. Generally it is indeed bad practice. When you do so, you are on your own. This means that you must have in-depth knowledge both of the hardware and of what the compiler will output. But when you know this, you are then free to make "wild" assumptions such as "pointers really are just a chunk of bytes, x bits long, which represents a computer address". Now if you _know_ that all pointers of the given system are of that format, the code is safe. – Lundin Nov 25 '14 at 09:01
  • The point I'm making here is that you often have to think outside the box which is the C standard, because there are many things in the standard that don't make any sense. For example, pointers are most often just addresses of fixed width, nothing magical about them. Signed integers are extremely likely to always be of two's complement format and nothing else. Integers are extremely unlikely to have padding bits. And so on. – Lundin Nov 25 '14 at 09:04
  • 1
    @Lundin this is WRONG. Yes you can known that all pointers are "just addresses" with the same width, but the compiler can also "know" that certain pointer types are not allowed to alias if they are dereferenced. The results can be disastrous. See http://davmac.wordpress.com/2009/10/25/mysql-and-c99-aliasing-rules-a-detective-story/ for a real-world example (this is my blog). – davmac Nov 25 '14 at 12:52
  • @davmac "This means that you must have in-depth knowledge both of the hardware and of what the compiler will output." – Lundin Nov 25 '14 at 13:31
  • 1
    @Lundin ... which makes it completely unsuitable as a general answer. It is not common for programmers to have a deep understanding of the compiler they use. Furthermore what you've quoted is in a comment on the answer, not in the answer itself. You should at least make it clear _in the answer_ that the compiler must have defined behaviour well beyond what the language spec actually requires. – davmac Nov 25 '14 at 17:26
  • 1
    @Lundin furthermore the assumptions are not just about the size and representation of pointers. Have you read the article I linked? Many compilers, unsurprisingly really, don't actually have a defined behaviour when Undefined Behavior is invoked. Your statement in the answer - "There are just two things that can go wrong here in practice" - is completely wrong. The compiler might re-order reads and writes to data it assumes cannot alias due to the aliasing rules. – davmac Nov 25 '14 at 17:34