In C language, is it semantically possible to create an lvalue with incomplete type?

786 views Asked by At

In the C89 standard, I found the following section:

3.2.2.1 Lvalues and function designators

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.

If I read it correctly, it allows us to create an lvalue and applies some operators on it, which compiles and can cause undefined behavior during runtime.

Problem is that, I can't think of an example of "an lvalue with incomplete type" which can pass compiler's semantic check and triggers undefined behavior.

Consider that an lvalue is

An lvalue is an expression (with an object type or an incomplete type other than void) that designates an object.

and that incomplete type is

Types are partitioned into object types (types that describe objects), function types (types that describe functions), and incomplete types (types that describe objects but lack information needed to determine their sizes).

A failed program I tried:

struct i_am_incomplete;
int main(void)
{
    struct i_am_incomplete *p;
    *(p + 1);
    return 0;
}

and got the following error:

error: arithmetic on a pointer to an incomplete type 'struct i_am_incomplete'
    *(p + 1);
      ~ ^

Anyone can think of an example on this ? An example of "an lvalue with incomplete type" which can pass compiler's semantic check and triggers undefined behavior.


UPDATE:

As @algrid said in the answer, I misunderstood undefined behavior, which contains compile error as an option.

Maybe I'm splitting hairs, I still wonder the underlying motivation here to prefer undefined behavior over disallowing an lvalue to have an incomplete type.

4

There are 4 answers

8
supercat On BEST ANSWER

Some build systems may have been designed in a way would allow code like:

extern struct foo x;
extern use_foo(struct foo x); // Pass by value

...
use_foo(x);

to be processed successfully without the compiler having to know or care about the actual representation of struct foo [for example, some systems may process pass-by-value by having the caller pass the address of an object and requiring the called function to make a copy if it's going to modify it].

Such a facility may be useful on systems that could support it, and I don't think the authors of the Standard wanted to imply that code which used that feature was "broken", but they also didn't want to mandate that all C implementations support such a feature. Making the behavior undefined would allow implementations to support it when practical, without requiring that they do so.

2
Jens Gustedt On

Sure, array types can be that:

extern double A[];
...
A[0] = 1;           // lvalue conversion of A

This has well defined behavior, even if the definition of A is not visible to the compiler. So inside this TU the array type is never completed.

14
algrid On

"Undefined behavior" term includes compilation error as an option. From the C89 standard:

Undefined behavior - behavior, upon use of a nonportable or erroneous program construct, of erroneous data, or of indeterminately-valued objects, for which the Standard imposes no requirements. Permissible undefined behavior ranges from ignoring the situation completely with unpredictable results, to behaving during translation or program execution in a documented manner characteristic of the environment (with or without the issuance of a diagnostic message), to terminating a translation or execution (with the issuance of a diagnostic message).

As you can see "terminating a translation" is ok.

In this case I believe the compilation error you get for you sample code is an example of "undefined behavior" implemented as compile time error.

11
M.M On

I believe this program demonstrates the case:

struct S;
struct S *s, *f();

int main(void)
{
    s = f();
    if ( 0 )
        *s;   // here
}

struct S { int x; };
struct S *f() { static struct S y; return &y; }

On the marked line, *s is an lvalue of incomplete type, and it does not fall under any of the "Except..." cases in your quote of 3.2.2.1 (which is 6.3.2.1/2 in the current standard). Therefore it is undefined behaviour.

I tried my program in gcc and clang and they both rejected it with the error that a pointer to incomplete type cannot be dereferenced; but I cannot find anywhere in the Standard which would make that a constraint violation, so I believe the compilers are incorrect to reject the program. Or possibly the standard is defective by omitting such a constraint, which would make sense.

(Since the code is inside an if(0), that means the compiler cannot reject it merely on the basis of it being undefined behaviour).