Возвращаемый массив, содержащий массив

Следующий простой код segfaults под 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);
}

Изменение последней строки на

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

Прекрасно работает. Обе версии работают при компиляции с помощью -std=c99. Я просто вызываю поведение undefined или что-то изменилось в стандарте, что позволяет коду работать под C99? Почему происходит сбой при C89?

Ответ 1

Я считаю, что поведение undefined как в C89/C90, так и в C99.

foo().f - выражение типа массива, в частности char[25]. C99. 6.3.2.1p3 говорит:

За исключением случаев, когда это операнд оператора sizeof или унарного & или представляет собой строковый литерал, используемый для инициализации массива, выражение, которое имеет тип "массив типа", преобразуется в выражение с типом "указатель на тип", указывающий на начальный элемент объекта массива и не является значением lvalue. Если объект массива имеет класс хранения регистра, поведение undefined.

Проблема в этом конкретном случае (массив, который является элементом структуры, возвращаемой функцией), состоит в том, что нет "объекта массива". Результаты функции возвращаются значением, поэтому результатом вызова foo() является значение типа struct Foo, а foo().f - значение (не lvalue) типа char[25].

Это, насколько мне известно, единственный случай в C (до C99), где вы можете иметь не-lvalue выражение типа массива. Я бы сказал, что поведение попытки доступа к нему undefined путем упущения, вероятно, потому, что авторы стандарта (понятно, ИМХО) не думали об этом случае. Вероятно, вы увидите различные варианты поведения при разных настройках оптимизации.

Новый 2011 C-стандарт исправляет этот угловой случай, изобретая новый класс хранения. N1570 (ссылка на поздний предварительный проект C11) говорит в 6.2.4p8:

Не-lvalue выражение со структурой или типом объединения, где структура или объединение содержит элемент с типом массива (включая, рекурсивно, члены всех содержащихся структур и союзов) относится к объект с автоматическим временем хранения и временным временем жизни. Его время жизни начинается, когда выражение оценивается и его начальный value - значение выражения. Его срок службы заканчивается, когда оценка содержания полного выражения или полного декларатора заканчивается. Любая попытка изменить объект с временным временем жизни приводит к undefined.

Таким образом, поведение программы хорошо определено в C11. Пока вы не сможете получить компилятор, совместимый с C11, тем не менее, лучший выбор - это, вероятно, сохранение результата функции в локальном объекте (при условии, что ваша цель - это рабочий код, а не разрыв компиляторов):

[...]
int main(void ) {
    struct Foo temp = foo();
    printf("%s\n", temp.f);
}

Ответ 2

printf немного смешно, потому что это одна из тех функций, которая принимает varargs. Поэтому давайте разбейте его, написав вспомогательную функцию bar. Мы вернемся к printf позже.

(Я использую "gcc (Ubuntu 4.4.3-4ubuntu5) 4.4.3" )

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

и вместо этого называет это:

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

ОК, что дает ошибку. В C и С++ вам не разрешается передавать массив по значению. Вы можете обойти это ограничение, поместив массив внутри структуры, например void bar2(Foo f) {...}

Но мы не используем это обходное решение - нам не разрешено передавать массив по значению. Теперь вы можете подумать, что он должен распадаться на char*, позволяя передать массив по ссылке. Но распад работает только в том случае, если массив имеет адрес (т.е. Является lvalue). Но временные, такие как возвращаемые значения из функции, живут на волшебной земле, где у них нет адреса. Поэтому вы не можете принять адрес & временного. Короче говоря, нам не разрешается принимать адрес временного, и, следовательно, он не может распадаться на указатель. Мы не можем передать его по значению (потому что это массив), ни по ссылке (потому что это временная).

Я обнаружил, что работает следующий код:

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

но, честно говоря, я думаю, что подозреваемый. Разве это не нарушало правила, которые я только что перечислил?

И чтобы быть полным, это работает отлично, как должно:

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

Переменная f не является временной и, следовательно, мы можем (неявно, во время распада) принимать свой адрес.

printf, 32-бит и 64-бит, а странность

Я обещал снова упомянуть printf. Согласно вышеизложенному, он должен отказаться передать foo(). F любой функции (включая printf). Но printf смешно, потому что это одна из тех функций vararg. gcc разрешил себе передать массив по значению в printf.

Когда я сначала скомпилировал и запустил код, он был в 64-битном режиме. Я не видел подтверждения моей теории, пока не скомпилировал ее в 32-битном (-m32 в gcc). Конечно, у меня есть segfault, как в исходном вопросе. (Я получал некоторый барабанный вывод, но без segfault, когда в 64 бит).

Я внедрил свой собственный my_printf (с бессмысленностью vararg), который напечатал фактическое значение char *, прежде чем пытаться напечатать буквы, на которые указывает char*. Я назвал его так:

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

и это результат, который я получил (код на ideone):

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

Первое значение указателя 0xffc14eb3 является правильным (оно указывает на символы "Hello, world!" ), но посмотрите на второй 0x6c6c6548. То, что ASCII кодирует для Hell (обратный порядок - небольшая сущность или что-то в этом роде). Он скопировал массив по значению в printf, и первые четыре байта были интерпретированы как 32-разрядный указатель или целое число. Этот указатель не указывает на смысл в любом месте, и, следовательно, программа вылетает, когда пытается получить доступ к этому местоположению.

Я думаю, что это нарушает стандарт, просто в силу того, что мы не должны позволять копировать массивы по значению.

Ответ 3

В MacOS X 10.7.2 оба GCC/LLVM 4.2.1 ('i686-apple-darwin11-llvm-gcc-4.2 (GCC) 4.2.1 (на основе Apple Inc. build 5658) (LLVM build 2335.15. 00) ') и GCC 4.6.1 (который я построил) компилируют код без предупреждений (под -Wall -Wextra), как в 32-битном, так и в 64-битном режимах. Все программы запускаются без сбоев. Это то, чего я ожидал; код выглядит хорошо для меня.

Может быть, проблема с Ubuntu является ошибкой в ​​конкретной версии GCC, которая с тех пор исправлена?