4

I have the following situation:

file A.c:

typedef struct element
{
    uint16_t value_raw;
    float value_scaled;
    char *desc;
} element;

element sv[REG_READ_COUNT];

file A.h:

typedef struct element element;

file B.c:

#include "A.h"
void dostuff (element sv[]) { } 

at compile time I get "error: array type has incomplete element type" for the function argument definition in B.c.

What's the right way to do this? How can I pass an array of type 'element' into a function?

4
  • Reproduced with gcc+clang: coliru.stacked-crooked.com/a/e5e314deef461290 Commented Sep 14, 2014 at 22:44
  • FWIW, bcc32 6.70 also accepts X *x and rejects X x[] Commented Sep 14, 2014 at 23:05
  • Here is one question that was never asked of the OP and it actually makes a difference in how one can answer. There are actually 2 solutions. One involves properly using opaque (incomplete) types and one doesn't. Both are valid ways of solving the problem. If this was part of a homework assignment and it said you must use data hiding and create an abstraction around element then there is no answer given here that would be acceptable. If no data hiding was needed than the OPs choice of answer makes sense. So the simple question to remove all ambiguity is - Is data hiding element required? Commented Sep 15, 2014 at 0:15
  • The definition of the struct format should be in the .h file. To have the instance of the struct visible everywhere, the instance declaration needs to be in the .h file. In your code the instance declaration is in the .h file, but the struct format is missing. Commented Sep 16, 2014 at 13:09

5 Answers 5

6

In B.c, element is an incomplete type (it is not defined in A.h, only in A.c). C disallows array declarators with incomplete element types (as you've discovered). Here's the relevant text from the C99 draft:

6.7.5.2 Array declarators

Constraints

  1. In addition to optional type qualifiers and the keyword static, the [ and ] may delimit an expression or *. If they delimit an expression (which specifies the size of an array), the expression shall have an integer type. If the expression is a constant expression, it shall have a value greater than zero. The element type shall not be an incomplete or function type. The optional type qualifiers and the keyword static shall appear only in a declaration of a function parameter with an array type, and then only in the outermost array type derivation.

Emphasis mine. This applies to all array declarators, no matter where they occur: in variable declarations, typedefs, function parameter lists, etc.

To fix your code, put the full struct definition in A.h. Or, if dostuff doesn't actually need to work with the elements (e.g. simply to pass the "array" to some other function), you could use void dostuff(element *sv).

Sign up to request clarification or add additional context in comments.

16 Comments

@Deduplicator: An array is not a pointer. Declaring an array is illegal if the size of the elements is not known, AFAIK, but declaring a pointer to unknown elements is legal. Can you prove otherwise?
@nneonneo What he is saying is you cuold pass an array of pointers to element structures.
@Deduplicator There is no array of an unknown bound. There is a pointer to element.
Gcc refuses to compile T t[] with -std=c89 and -std=c99. I'm confused, C11 (n1570) 6.7.6.3 p4 reads (in a "constraints" section): After adjustment, the parameters in a parameter type list in a function declarator that is part of a definition of that function shall not have incomplete type. As I understand it, this "after adjustment" refers to ibid. p7 A declaration of a parameter as "array of type" shall be adjusted to "qualified pointer to type", [...] Interesting.
@Deduplicator: OK folks, I have amended the answer. Your compilers are NOT wrong, the standard is quite clear that incomplete element types are simply illegal in array declarators.
|
2

Minimal code to reproduce the error.

struct element;
void dostuff (struct element sv[]) { } 

Testing on clang and gcc using coliru: http://coliru.stacked-crooked.com/a/e5e314deef461290
Result: GCC and clang always complain about arguments of type array of incomplete type, and never about pointer to incomplete type.

Relevant standard-quotes:

6.7.6.3 Function declarators (including prototypes)

[...]
4 After adjustment, the parameters in a parameter type list in a function declarator that is part of a definition of that function shall not have incomplete type.
[...]
7 A declaration of a parameter as ‘‘array of type’’ shall be adjusted to ‘‘qualified pointer to type’’, where the type qualifiers (if any) are those specified within the [ and ] of the array type derivation. If the keyword static also appears within the [ and ] of the array type derivation, then for each call to the function, the value of the corresponding actual argument shall provide access to the first element of an array with at least as many elements as specified by the size expression.

Well, up to here it looks like array of incomplete type was perfectly fine for an argument type, even in a definition.

6.2.5 Types

[...]
20 Any number of derived types can be constructed from the object and function types, as follows:

  • An array type describes a contiguously allocated nonempty set of objects with a particular member object type, called the element type. The element type shall be complete whenever the array type is specified. Array types are characterized by their element type and by the number of elements in the array. An array type is said to be derived from its element type, and if its element type is T, the array type is sometimes called ‘‘array of T’’. The construction of an array type from an element type is called ‘‘array type derivation’’.

The above quote explicitly disallows using array-syntax with an incomplete type, for every case.

Conclusion: All those compilers seem to be right, even though that restriction seems unneeded.


Anyway, the proper course is not putting a forward-declaration for the type, but the declaration for the type itself into the header-file, unless it shall be an opaque type.

In that case, you will have to use pointer-syntax for the argument-type directly.

Comments

1

As a secondary answer to provide a method to do what the OP wanted but assuming he needed data hiding, I present this code that builds on my first answer and provides generic access to an element type in one C file and providing only an opaque data type in the header file. Please note that to get a point across about what are pointers I use element * however they could have all been replaced by ELEM_HANDLE that I define as a type in the header. ELEM_HANDLE abstracts away the fact that we are dealing with element pointers. Since we use an opaque type we make available methods that can be called (defined in element.h) to work on our opaque type.

element.h:

#include <stdint.h>

typedef struct element element;
typedef element *ELEM_HANDLE;

extern element *element_new();
extern void element_delete(element *elem);
extern void element_set_value_raw(element *elem, uint16_t value_raw);
extern uint16_t element_get_value_raw(element *elem);
extern void element_set_value_scaled(element *elem, float value_scaled);
extern float element_get_value_scaled(element *elem);
extern void element_set_desc(element *elem, char *desc);
extern char *element_get_desc(element *elem);

element.c:

#include <stdint.h>
#include <stdlib.h>

typedef struct element
{
        uint16_t value_raw;
        float value_scaled;
        char *desc;
} element;

element *element_new()
{
        return calloc(1, sizeof(element));
}

void element_delete(element *elem)
{
        free(elem);
}

void element_set_value_raw(element *elem, uint16_t value_raw)
{
        elem->value_raw = value_raw;
}
uint16_t element_get_value_raw(element *elem)
{
        return elem->value_raw;
}

void element_set_value_scaled(element *elem, float value_scaled)
{
        elem->value_scaled = value_scaled;
}

float element_get_value_scaled(element *elem)
{
        return elem->value_scaled;
}

void element_set_desc(element *elem, char *desc)
{
        elem->desc = desc;
}

char *element_get_desc(element *elem)
{
        return elem->desc;
}

testelem.c:

#include <stdio.h>
#include "element.h"

#define REG_READ_COUNT 2

void dostuff(element *sv[], int arrLen)
{
        int index;
        element *curelem;
        uint16_t raw;
        float scaled;
        char *desc;

        for (index = 0; index < arrLen ; index++){
                curelem = sv[index];
                raw = element_get_value_raw(curelem);
                scaled = element_get_value_scaled(curelem);
                desc = element_get_desc(curelem);
                /* Do more interesting stuff here */
                printf("%s, %d, %.4f\n", desc, raw, scaled);
        }
}

int main()
{
        unsigned int index;
        element *sv[REG_READ_COUNT]; /* array of element pointers*/
        char desc1[] = "The answer to everything";
        char desc2[] = "OtherStuff";

        /* Initialize an array of pointers to element items */
        for (index = 0; index < sizeof(sv) / sizeof(element *); index++)
                sv[index] = element_new();

        element_set_value_raw(sv[0], 42);
        element_set_value_scaled(sv[0], 6.66f);
        element_set_desc(sv[0], desc1);
        element_set_value_raw(sv[1], 123);
        element_set_value_scaled(sv[1], 456.7f);
        element_set_desc(sv[1], desc2);

        dostuff(sv, REG_READ_COUNT);

        /* free the array of pointers to element items*/
        for (index = 0; index < sizeof(sv) / sizeof(element *); index++)
                element_delete(sv[index]);

        return 0;
}

Note that I took liberties to pass in the array length to dostuff beside the array of element pointers. This provides dostuff with enough information to determine how many elements are in the array. This should compile (and run) properly on C89 or above and C++ compilers (provided you rename the .c files to .cpp).

I present this answer because using forward declarations and opaque types is how many "C" language shared objects are created. This mechanism allows the element source to be compiled into an independent library or shared object and used without knowing what the element data type looks like. In essence we provide an interface contract between the modules that use us and the library. If we modify the internals of the structure element in element.cpp our modules that use it will not need to be recompiled (just re-linked). client code that uses the library would need to be rebuilt if we modify the interface(contract).

So in the end forward references (opaque types) can be used to hide C data type internals and provide a layer of abstraction. This type of mechanism is often used by shared objects (.so files) to build complex libraries that can be used by C programs.

Comments

0

Your compilation error is described by Deduplicator's answer.

You can work around the issue by writing element *sv. However, B.c can only see the definition typedef struct element element;. It can't see what makes up the element.

If the "real" version of dostuff does anything with sv that requires knowing what that struct actually contains, then you need to move the definition of struct element from A.c into A.h.

15 Comments

Comment was: that requires knowing what that struct actually contains. That is true however it also entails any operation that needs to know the size of the object. If element was a complete type then argument element *sv allows one to do this: sv++ to get to the next array element. If element is incomplete and the argument is element *sv then the programmer will be greeted with an error when he does sv++. This is because array arithmetic needs to know the size (even though it doesn't care about what makes up an element
The size of a struct is determined by its contents; there is no way to specify the size of a struct without having the full struct definition available. Therefore sv++ requires knowing what the struct contains.
The compiler will take the type and determine its size from the contents up front. After that when it comes to doing the array arithmetic the contents don't matter but the size does. In theory if this were a union the contents cold be representative of many different things. The maximum size of the union + alignment will equal a size. But for array computation what is in it doesn't matter.
@MichaelPetch the size cannot be determined without knowing the contents . Array computation requires the contents to be visible. If you disagree then please post a program where array computation is performed, but the contents are not visible.
The point you are missing is that the size is determined by the content. We both agree on that. The incomplete type the size is not known. However from the compilers perspective when doign array arithmetic it doesn't peek inside the structure anymore. It makes determination on size alone. It could be 100 chars or 25 4 byte ints. Array arithmetic doesn't care about the actual contents anymore. That is my point and the only clarification I was adding.
|
0

Because A.h only defines an opaque type typedef struct element element B.c can't possibly know the make up of element to even determine its size. So it can't create an array of those structures. If you want this code to work you would have to move the entire typedef in A.c to A.h . If you do this then there is no information hiding and the full structure is available through the header.

Additionally, You could create an array of pointers to structures (even though it may be incomplete) and pass it into your function but you wouldn't be able to access any of the structures member variables directly.

An example of using an opaque data type in arrays of pointers to those types:

typedef struct element element;
#define REG_READ_COUNT 100

void dostuff (element *sv[])
{
    sv++; /* get next pointer to element */
};

int main()
{
    element *sv[REG_READ_COUNT]; /* array of pointers to element */
    dostuff(sv);
}

This code is fine until it needs anything requiring the sizeof the actual type. We can't even initialize the data members to anything without extra glue code (another module) that actually does have access to the complete element type.

The reason you can have arrays of pointers (even to incomplete types) is because a pointer is a basic type in C. It is neither an incomplete type or a function type. A pointer has fixed size that the compiler can use to generate pointer arrays.

6.7.5.2 Array declarators

Constraints

In addition to optional type qualifiers and the keyword static, the [ and ] may delimit an expression or *. If they delimit an expression (which specifies the size of an array), the expression shall have an integer type. If the expression is a constant expression, it shall have a value greater than zero. The element type shall not be an incomplete or function type. The optional type qualifiers and the keyword static shall appear only in a declaration of a function parameter with an array type, and then only in the outermost array type derivation.

Because pointers are not incomplete types or function types you can create arrays of them even if they point to incomplete types. A pointer to an incomplete type doesn't make the pointer somehow incomplete. You just can't de-reference it and hope to do anything useful with it directly. I say directly because in data hiding techniques and opaque pointers you can provide indirect mechanisms to work with the opaque pointers data..

Here is an example of code that should fail to compile in a similar way the OPs did. We take the idea that pointers to incomplete types can be passed around (function arguments) but they still can't be used as an array inside a function:

typedef struct element element;
#define REG_READ_COUNT 100

void dostuff (element *sv) /* Completely legal but useless if you intend to use it as an array */
{
    sv++; /* This should fail - as we are doing array arithmetic on
           * an incomplete type. Can't find the starting point of the next
           * array element without knowing the size of the object */
};

int main()
{
    element sv[REG_READ_COUNT]; /* array of elements will also fail - size of object unknown */
    dostuff(sv);
}

This is nearly identical to the previous one. In this one we have a pointer sv to an incomplete type as a function argument (this is from nneonneo answer). This is completely legal since it is just a pointer. However attempting to do array arithmetic on it (using ++ in the body function) will fail because it needs to know the size of element and it isn't known. ++ and -- or to index an array are undefined behavior (and most standard compliant compilers will throw an error). ISO/IEC 9899:TC2 says:

6.3.2 Other operands

6.3.2.1 Lvalues, arrays, and function designators

...

2 Except when it is the operand of the sizeof operator, the unary & operator, the ++ operator, the -- operator, or the left operand of the . operator or an assignment operator, an lvalue that does not have array type is converted to the value stored in the designated object (and is no longer an lvalue). If the lvalue has qualified type, the value has the unqualified version of the type of the lvalue; otherwise, the value has the type of the lvalue. If the lvalue has an incomplete type and does not have array type, the behavior is undefined

More on opaque types can be found here

5 Comments

Where in B.c is any element of type element defined, quite aside from a whole array of them?
No not an array of elements. An array of pointers to elements. That can be done because the size of a pointer is known at compile time.
Please provide a standards quote that proofs the point. Compiler-errors happen...
I will find references to the standard. However you should note I didn't define dostuff as nneonneo suggested. There is a difference between his function prototype and mine. The reason this code works though is because at no point does my code need anything other than pointers to things. Pointers have a fixed size so it is completely legal.
Then prove that the extra indirection is neccessary to make it legal, and the compilers aren't in violation of the standard.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.