28

The following simple code segfaults under gcc 4.4.4

#include<stdio.h>

typedef struct Foo Foo;
struct Foo {
    char f[25];
};

Foo foo(){
    Foo f = {"Hello, World!"};
    return f;
}

int main(){
    printf("%s\n", foo().f);
}

Changing the final line to

 Foo f = foo(); printf("%s\n", f.f);

Works fine. Both versions work when compiled with -std=c99. Am I simply invoking undefined behavior, or has something in the standard changed, which permits the code to work under C99? Why does is crash under C89?

Mysticial
  • 464,885
  • 45
  • 335
  • 332
Dave
  • 10,964
  • 3
  • 32
  • 54
  • 4
    If you turn on warnings, I get `warning: format ‘%s’ expects type ‘char *’, but argument 2 has type ‘char[25]’` – Aaron McDaid Jan 08 '12 at 01:54
  • Remember that `printf` is one of those funny vararg functions. If it was being passed to a normal function that took a `char*` argument, then it should decay nicely. But I think it's interacting funny with `printf`. – Aaron McDaid Jan 08 '12 at 01:59
  • It doesn't `Invalid use of non-lvalue array` – Dave Jan 08 '12 at 02:02
  • I'm on "gcc (Ubuntu 4.4.3-4ubuntu5) 4.4.3". What are you using? – Aaron McDaid Jan 08 '12 at 02:06
  • In C++, that would be OK. You can call members of a temporary. But maybe C has different rules about temporaries. – Aaron McDaid Jan 08 '12 at 02:08
  • You know, I don't think I've every seen a construction like `foo().f` in a *c* program before. It would be very natural in a c++ program, of course. – dmckee --- ex-moderator kitten Jan 08 '12 at 02:10
  • 2
    In *C*, is there anything that you are allowed to do with a temporary, other than copy it by value into another location (into a function or into a local variable)? Maybe the answer is No. – Aaron McDaid Jan 08 '12 at 02:11
  • Added the "language-lawyer" tag. Since we seem to be needing one of those to answer this... – Mysticial Jan 08 '12 at 02:12
  • @AaronMcDaid that warning happens in C89 mode, but not c99. And, interestingly, only in the first construction, not after storing it. – Kevin Jan 08 '12 at 04:16

3 Answers3

20

I believe the behavior is undefined both in C89/C90 and in C99.

foo().f is an expression of array type, specifically char[25]. C99 6.3.2.1p3 says:

Except when it is the operand of the sizeof operator or the unary & operator, or is a string literal used to initialize an array, an expression that has type "array of type" is converted to an expression with type "pointer to type" that points to the initial element of the array object and is not an lvalue. If the array object has register storage class, the behavior is undefined.

The problem in this particular case (an array that's an element of a structure returned by a function) is that there is no "array object". Function results are returned by value, so the result of calling foo() is a value of type struct Foo, and foo().f is a value (not an lvalue) of type char[25].

This is, as far as I know, the only case in C (up to C99) where you can have a non-lvalue expression of array type. I'd say that the behavior of attempting to access it is undefined by omission, likely because the authors of the standard (understandably IMHO) didn't think of this case. You're likely to see different behaviors at different optimization settings.

The new 2011 C standard patches this corner case by inventing a new storage class. N1570 (the link is to a late pre-C11 draft) says in 6.2.4p8:

A non-lvalue expression with structure or union type, where the structure or union contains a member with array type (including, recursively, members of all contained structures and unions) refers to an object with automatic storage duration and temporary lifetime. Its lifetime begins when the expression is evaluated and its initial value is the value of the expression. Its lifetime ends when the evaluation of the containing full expression or full declarator ends. Any attempt to modify an object with temporary lifetime results in undefined behavior.

So the program's behavior is well defined in C11. Until you're able to get a C11-conforming compiler, though, your best bet is probably to store the result of the function in a local object (assuming your goal is working code rather than breaking compilers):

[...]
int main(void ) {
    struct Foo temp = foo();
    printf("%s\n", temp.f);
}
Keith Thompson
  • 254,901
  • 44
  • 429
  • 631
  • +1. A question though. Does the C11 standard say anything about the constness of these expressions? I'm guessing you can only take a `const` pointer to them? e.g. `bar(&foo());` and `baz(foo().f);` where bar and baz are `void bar(const Foo *);` and `void baz(const char *);` – Aaron McDaid Jan 08 '12 at 21:08
  • @AaronMcDaid: I don't think so. If it were `const`, it wouldn't be necessary to day that attempting to modify it has undefined behavior. – Keith Thompson Jan 08 '12 at 21:19
  • I'd interpret that the other way around. (But I think it's a bit ambiguous.) I think it's "The pointer is to a const type, and if you cast it to a non-const type and try to modify through it, then the behaviour is undefined". My logic is: if you're not supposed to modify it, shouldn't the type be `const`? – Aaron McDaid Jan 08 '12 at 21:24
  • .. but, in short, that final sentence answers my underlying question. Thanks @KeithThompson – Aaron McDaid Jan 08 '12 at 21:24
  • @AaronMcDaid: It's like string literals: they're not `const`, but modifying them is undefined. (That's for historical reasons; I'm not sure why they didn't make function results `const`.) – Keith Thompson Jan 08 '12 at 21:26
13

printf is a bit funny, because it's one of those functions that takes varargs. So let's break it down by writing a helper function bar. We'll return to printf later.

(I'm using "gcc (Ubuntu 4.4.3-4ubuntu5) 4.4.3")

void bar(const char *t) {
    printf("bar: %s\n", t);
}

and calling that instead:

bar(foo().f); // error: invalid use of non-lvalue array

OK, that gives an error. In C and C++, you are not allowed to pass an array by value. You can work around this limitation by putting the array inside a struct, for example void bar2(Foo f) {...}

But we're not using that workaround - we're not allowed to pass in the array by value. Now, you might think it should decay to a char*, allowing you to pass the array by reference. But decay only works if the array has an address (i.e. is an lvalue). But temporaries, such as the return values from function, live in a magic land where they don't have an address. Therefore you can't take the address & of a temporary. In short, we're not allowed to take the address of a temporary, and hence it can't decay to a pointer. We are unable to pass it by value (because it's an array), nor by reference (because it's a temporary).

I found that the following code worked:

bar(&(foo().f[0]));

but to be honest I think that's suspect. Hasn't this broken the rules I just listed?

And just to be complete, this works perfectly as it should:

Foo f = foo();
bar(f.f);

The variable f is not a temporary and hence we can (implicitly, during decay) takes its address.

printf, 32-bit versus 64-bit, and weirdness

I promised to mention printf again. According to the above, it should refuse to pass foo().f to any function (including printf). But printf is funny because it's one of those vararg functions. gcc allowed itself to pass the array by value to the printf.

When I first compiled and ran the code, it was in 64-bit mode. I didn't see confirmation of my theory until I compiled in 32-bit (-m32 to gcc). Sure enough I got a segfault, as in the original question. (I had been getting some gibberish output, but no segfault, when in 64 bits).

I implemented my own my_printf (with the vararg nonsense) which printed the actual value of the char * before trying to print the letters pointed at by the char*. I called it like so:

my_printf("%s\n", f.f);
my_printf("%s\n", foo().f);

and this is the output I got (code on ideone):

arg = 0xffc14eb3        // my_printf("%s\n", f.f); // worked fine
string = Hello, World!
arg = 0x6c6c6548        // my_printf("%s\n", foo().f); // it's about to crash!
Segmentation fault

The first pointer value 0xffc14eb3 is correct (it points to the characters "Hello, world!"), but look at the second 0x6c6c6548. That's the ASCII codes for Hell (reverse order - little endianness or something like that). It has copied the array by value into printf and the first four bytes have been interpreted as a 32-bit pointer or integer. This pointer doesn't point anywhere sensible and hence the program crashes when it attempts to access that location.

I think this is in violation of the standard, simply by virtue of the fact that we're not supposed to be allowed to copy arrays by value.

Aaron McDaid
  • 26,501
  • 9
  • 66
  • 88
  • 1
    It seems like, unable to decay, printf actually gets passed the array by value. I'm not implying that the behavior is legal, but `printf("%c%c%c%c\n", foo().f);` prints every `sizeof(int)`th character in the string. I'm really interested in finding some reference in the standard to clear this up. – Dave Jan 08 '12 at 02:38
  • I just noticed the same thing. See the end of my answer now. – Aaron McDaid Jan 08 '12 at 02:51
0

On MacOS X 10.7.2, both GCC/LLVM 4.2.1 ('i686-apple-darwin11-llvm-gcc-4.2 (GCC) 4.2.1 (Based on Apple Inc. build 5658) (LLVM build 2335.15.00)') and GCC 4.6.1 (which I built) compile the code without warnings (under -Wall -Wextra), in both 32-bit and 64-bit modes. The programs all run without crashing. This is what I'd expect; the code looks fine to me.

Maybe the problem on Ubuntu is a bug in the specific version of GCC that has since been fixed?

Jonathan Leffler
  • 730,956
  • 141
  • 904
  • 1,278
  • I just tried with gcc 4.6.2 (compiled myself) on Ubuntu. I got a segfault again, but output was slightly different. I didn't get the `0x6c6c6548 == "Hell"` I got in my other answer. I got `0xffffffff`. This is all pretty weird. – Aaron McDaid Jan 08 '12 at 03:58
  • I dug out a SuSE VM (Linux ids1150srvr 2.6.16.60-0.21-smp #1 SMP Tue May 6 12:41:02 UTC 2008 i686 i686 i386 GNU/Linux) and GCC 4.1.2 and the test program compiled without warnings and ran successfully. So, the problem is definitely somewhat system-specific. It certainly isn't simply Linux vs the rest. That's no extra help though. If you're invoking undefined behaviour (despite my thoughts that you probably aren't), then anything can happen and it is valid. – Jonathan Leffler Jan 08 '12 at 05:55