Что означает невозможность возврата массивов на C?

Я не пытаюсь воспроизвести обычный вопрос о том, что C не может возвращать массивы, но чтобы углубиться в него.

Мы не можем этого сделать:

char f(void)[8] {
    char ret;
    // ...fill...
    return ret;
}

int main(int argc, char ** argv) {
    char obj_a[10];
    obj_a = f();
}

Но мы можем сделать:

struct s { char arr[10]; };

struct s f(void) {
    struct s ret;
    // ...fill...
    return ret;
}

int main(int argc, char ** argv) {
    struct s obj_a;
    obj_a = f();
}

Таким образом, я сбрасывал код ASM, созданный gcc -S, и, похоже, работает со стеком, адресуя -x(%rbp) как и с любой другой функцией возврата функции.

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

Дополнительные данные: Я использую Linux и gcc на x64 Intel.

Ответ 1

Прежде всего, да, вы можете инкапсулировать массив в структуру, а затем делать все, что захотите, с этой структурой (назначить ее, вернуть ее из функции и т.д.).

Во-вторых, как вы уже выяснили, у компилятора не так много проблем, что он испускает код для возврата (или назначения) структур. Так что не причина, по которой вы не можете возвращать массивы.

Основная причина, по которой вы не можете сделать этого, состоит в том, что, строго говоря, массивы - это структуры данных второго класса в C. Все остальные структуры данных являются первоклассными. Каковы определения "первоклассного" и "второго класса" в этом смысле? Просто, что типы второго класса не могут быть назначены.

(Возможно, следующий вопрос: "Помимо массивов, есть ли другие типы данных второго класса?", И я думаю, что ответ "Не совсем, если вы не подсчитываете функции".)

Тесно связанный с тем фактом, что вы не можете возвращать (или назначать) массивы, заключается в том, что также нет значений типа массива. Существуют объекты (переменные) типа массива, но всякий раз, когда вы пытаетесь принять значение единицы, вы получаете указатель на первый элемент массива. [Сноска: более формально нет rvalues типа массива, хотя объект типа массива можно рассматривать как lvalue, хотя и не назначаемый.]

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

char a[10], b[10];
a = b;

как будто вы написали

a = &b[0];

Итак, у нас есть указатель справа и массив слева, и у нас будет массивное несоответствие типов, даже если массивы каким-то образом были назначены. Аналогично (из вашего примера), если мы попытаемся написать

a = f();

и где-то внутри определения функции f() имеем

char ret[10];
/* ... fill ... */
return ret;

это как будто последняя строка сказала

return &ret[0];

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

(В примере с вызовом функции мы также получили очень значительную проблему: ret - это локальный массив, опасный для возврата в C. Еще об этом позже.)

Теперь часть вашего вопроса, вероятно, "Почему это так?", А также "Если вы не можете назначать массивы, почему вы можете назначать структуры, содержащие массивы?"

Ниже следует мое толкование и мое мнение, но это согласуется с тем, что описывает Деннис Ритчи в статье "Развитие языка С".

Неприемлемость массивов возникает из трех фактов:

  1. C предназначен для синтаксически и семантически близкого к аппаратным средствам машины. Элементарная операция в C должна составлять до одной или нескольких машинных команд, принимая одну или несколько процессорных циклов.

  2. Массивы всегда были особенными, особенно в том, как они относятся к указателям; эта специальная связь развивалась и в значительной степени зависела от обработки массивов на языке предшественников B.

  3. Структуры первоначально не были на C.

Из-за пункта 2 невозможно назначить массивы, и из-за пункта 1 это не должно быть возможным в любом случае, поскольку один оператор присваивания = не должен расширяться до кода, который может занять N тысяч циклов для копирования массива N тысяч элементов,

И затем мы переходим к пункту 3, который действительно вызывает противоречие.

Когда C получили структуры, они изначально не были полностью первоклассными, поскольку вы не могли их назначить или вернуть. Но причина, по которой вы не могли, заключалась в том, что первый компилятор был недостаточно умным, чтобы сначала создать код. Не было синтаксического или семантического контрольно-пропускного пункта, как и для массивов.

И цель в целом заключалась в том, чтобы структуры были первоклассными, и это было достигнуто относительно рано, примерно в то время, когда печаталось первое издание K & R.

Но остается большой вопрос, если элементарная операция должна скомпилироваться с небольшим количеством инструкций и циклов, почему этот аргумент не запрещает назначение структуры? И ответ: да, это противоречие.

Я считаю (хотя это и является более спекуляцией с моей стороны), что мышление было примерно таким: "Первоклассные типы хороши, типы второго класса неудачны. Мы застряли со статусом второго класса для массивов, но мы можем лучше делать с конструкциями. Правило без дорогого кода на самом деле не правило, оно больше ориентировано. Массивы часто бывают большими, но структуры обычно будут маленькими, десятками или сотнями байтов, поэтому их назначение не будет как правило, слишком дорого ".

Поэтому последовательное применение правила без дорогого кода упало на обочине. Во всяком случае, C никогда не был абсолютно регулярным или последовательным. (И в этом отношении, как правило, подавляющее большинство успешных языков, как человеческих, так и искусственных).

Со всем этим сказано, может быть, стоит спросить: "Что, если C действительно поддерживает назначение и возврат массивов? Как это может работать?" И ответ должен будет каким-то образом отключить поведение массивов по умолчанию в выражениях, а именно, что они, как правило, превращаются в указатели на их первый элемент.

Когда-то в 90-х, IIRC, было довольно продуманное предложение сделать именно это. Я думаю, что это включало включение выражения массива в [ ] или [[ ]] или что-то в этом роде. Сегодня я не могу найти упоминания об этом предложении (хотя я был бы признателен, если бы кто-то мог предоставить ссылку). Пока я собираюсь выдвинуть гипотезу о новом операторе или arrayval() называемом arrayval().

Мы могли бы расширить C, чтобы разрешить назначение массива, выполнив следующие действия:

  1. Удалите запрет на использование массива в левой части оператора присваивания.

  2. Удалите запрет на объявление функций с использованием массива. Возвращаясь к исходному вопросу, сделайте char f(void)[8] {... } законным.

  3. (Это biggie.) Имейте способ упоминания массива в выражении и заканчивая истинным присваиваемым значением (rvalue) типа массива. Как уже упоминалось, на данный момент я собираюсь установить синтаксис arrayval(... ).

[Замечание: Сегодня у нас есть " ключевое определение ", которое

Ссылка на объект типа массива, который появляется в выражении, распадается (с тремя исключениями) на указатель на его первый элемент.

Три исключения состоят в том, что массив является операндом оператора sizeof или & или является инициализатором строкового литерала для массива символов. В гипотетических модификациях, которые я обсуждаю здесь, было бы четыре исключения, причем в список добавлен операнд оператора arrayval.]

Во всяком случае, с этими изменениями мы могли бы написать такие вещи, как

char a[8], b[8] = "Hello";
a = arrayval(b);

(Очевидно, нам также нужно будет решить, что делать, если a и b не имеют одинакового размера.)

Учитывая прототип функции

char f(void)[8];

мы могли бы также сделать

a = f();

Пусть смотреть на f гипотетического определения. У нас может быть что-то вроде

char f(void)[8] {
    char ret[8];
    /* ... fill ... */
    return arrayval(ret);
}

Обратите внимание, что (за исключением гипотетического нового оператора arrayval()) это именно то, что первоначально опубликовал Дарио Родригес. Также обратите внимание, что в гипотетическом мире, где назначение массива было законным, и что-то вроде arrayval() существовало - это действительно сработало бы! В частности, это не повлечет за собой проблему возврата скользящего недопустимого указателя на локальный массив ret. Он вернул бы копию массива, поэтому не было бы никакой проблемы - это было бы совершенно аналогично явно юридическим

int g(void) {
    int ret;
    /* ... compute ... */
    return ret;
}

Наконец, возвращаясь к вопросу о "Есть ли какие-либо другие типы второго класса?", Я думаю, что это больше, чем просто совпадение, что функции, такие как массивы, автоматически принимают свой адрес, когда они не используются сами по себе (то есть, как функции или массивы) и что аналогично нет значений типа функции. Но это, в основном, простуда, потому что я не думаю, что когда-либо слышал функции типа "второго класса" в C. (Возможно, они есть, и я забыл).

Ответ 2

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

Это не имеет никакого отношения к возможностям как таковым. Другие языки предоставляют возможность возвращать массивы, и вы уже знаете, что в C вы можете вернуть структуру с членом массива. С другой стороны, другие языки имеют то же ограничение, что и C, и тем более. Например, Java не может возвращать массивы, а также объекты любого типа из методов. Он может возвращать только примитивы и ссылки на объекты.

Нет, это просто вопрос языкового дизайна. Как и в большинстве других вещей, связанных с массивами, здесь пункты дизайна вращаются вокруг представления C, что выражения типа массива автоматически преобразуются в указатели практически во всех контекстах. Значение, указанное в операторе return не является исключением, поэтому C не может даже выразить возврат самого массива. Можно было бы сделать другой выбор, но этого просто не было.

Ответ 3

Для того, чтобы массивы были первоклассными объектами, вы, по крайней мере, могли бы их назначить. Но для этого требуется знание размера, а система типа C недостаточно мощна, чтобы прикрепить размеры к любым типам. C++ может это сделать, но не из-за устаревших проблем - он ссылается на массивы определенного размера (typedef char (&some_chars)[32]), но простые массивы по-прежнему неявно преобразуются в указатели, как в C. [CN10 ] имеет std :: array, а это, в основном, вышеупомянутый массив внутри структуры плюс некоторый синтаксический сахар.

Ответ 4

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

Возвращение структуры означает, что корневая структура изменяется с помощью скрытности в глубинах последовательности вызовов или дублирования данных и передачи больших фрагментов дублированных данных. Основные приложения C по-прежнему в основном сосредоточены вокруг глубоких встроенных приложений. В этих доменах есть небольшие процессоры, которым не нужно передавать большие блоки данных. У вас также есть инженерная практика, которая требует необходимости иметь возможность работать без динамического распределения RAM и с минимальным стеком и часто без кучи. Можно утверждать, что возврат структуры такой же, как модификация с помощью указателя, но абстрагирован в синтаксисе... Боюсь, я бы сказал, что не в философии C "то, что вы видите, что вы получаете" в Точно так же указатель на тип.

Лично я бы сказал, что вы нашли дыру в петле, независимо от того, одобрен ли стандарт или нет. C разработан таким образом, что выделение является явным. Вы передаете в качестве адреса объектов с высокой пропускной способностью адреса, обычно в желательном одном цикле, ссылаясь на память, которая была явно выделена в контролируемое время в рамках разработчиков ken. Это имеет смысл с точки зрения эффективности кода, эффективности циклов и обеспечивает наибольший контроль и ясность цели. Боюсь, при проверке кода я бы выбрал функцию, возвращающую структуру как плохую практику. C не применяет многие правила, это язык для профессиональных инженеров во многом, поскольку он полагается на пользователя, соблюдающего собственную дисциплину. Просто потому, что вы можете, это не значит, что вы должны... Он предлагает некоторые довольно пуленепробиваемые способы обработки данных очень сложного размера и типа, использующих строгость времени компиляции и минимизирующих динамические изменения отпечатка и во время выполнения.