Почему многие функции, которые возвращают структуры в C, фактически возвращают указатели на структуры?


49

В чем преимущество возврата указателя на структуру по сравнению с возвратом всей структуры в returnвыражении функции?

Я говорю о таких функциях, как fopenи другие низкоуровневые функции, но, вероятно, есть функции более высокого уровня, которые также возвращают указатели на структуры.

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

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

Я полагаю, что вернуть полную структуру NULLбудет сложнее или менее эффективно. Это веская причина?


10
@ JohnR.Strohm Я попробовал, и это на самом деле работает. Функция может вернуть структуру .... Так в чем причина не сделано?
yoyo_fun

28
Предварительная стандартизация C не позволяла копировать структуры или передавать их по значению. В стандартной библиотеке C есть много несогласных с той эпохи, которые не были бы написаны таким образом сегодня, например, потребовалось до C11, чтобы удалить совершенно неправильно разработанную gets()функцию. Некоторые программисты по-прежнему испытывают отвращение к копированию структур, старые привычки сильно умирают.
Амон

26
FILE*фактически является непрозрачной ручкой. Код пользователя не должен заботиться о его внутренней структуре.
CodesInChaos

3
Возврат по ссылке является разумным значением по умолчанию только при наличии сборки мусора.
Идан Арье

7
@ JohnR.Strohm «Очень старший» в вашем профиле, кажется, восходит к 1989 году ;-) - когда ANSI C разрешил то, что K & R C не допустил: копировать структуры в присваиваниях, передаче параметров и возвращаемых значениях. В оригинальной книге K & R действительно было четко сказано (я перефразирую): «Вы можете сделать со структурой ровно две вещи, взять ее адрес & и получить доступ к члену .».
Питер - Восстановить Монику

Ответы:


61

Есть несколько практических причин, по которым такие функции, как fopenуказатели возврата, вместо экземпляров structтипов:

  1. Вы хотите скрыть представление structтипа от пользователя;
  2. Вы выделяете объект динамически;
  3. Вы ссылаетесь на один экземпляр объекта через несколько ссылок;

В случае типов , такие как FILE *, это потому , что вы не хотите подвергать детали представления типа - к пользователю - это FILE *объект служит непрозрачной ручкой, и вы просто передать эту ручку для различных процедур ввода / вывода (и в то время как FILEэто часто реализовано как structтип, это не обязательно ).

Итак, вы можете выставить неполный struct тип где-нибудь в заголовке:

typedef struct __some_internal_stream_implementation FILE;

Хотя вы не можете объявить экземпляр неполного типа, вы можете объявить указатель на него. Так что я могу создать FILE *и назначить к нему через fopen, freopenи т.д., но я не могу напрямую манипулировать объект он указывает.

Также вероятно, что fopenфункция выделяет FILEобъект динамически, используя mallocили подобный. В этом случае имеет смысл вернуть указатель.

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


31
Особое преимущество использования указателя в качестве непрозрачного типа заключается в том, что сама структура может меняться между версиями библиотеки, и вам не нужно перекомпилировать вызывающие.
Бармар

6
@Barmar: Действительно, ABI Стабильность огромная точка продажи С, и это не будет столь же стабильным без непрозрачных указателей.
Матье М.

37

Есть два способа «вернуть структуру». Вы можете вернуть копию данных, или вы можете вернуть ссылку (указатель) на них. Обычно предпочитают возвращать (и вообще обходить) указатель по нескольким причинам.

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

Во-вторых, независимо от того, сколько раз вы копируете указатель, он все равно указывает на одну и ту же структуру в памяти. Все модификации к нему будут отражены в одной структуре. Но если вы копируете саму структуру, а затем вносите изменения, изменение отображается только в этой копии . Любой код, который содержит другую копию, не увидит изменения. Иногда, очень редко, это то, что вы хотите, но в большинстве случаев это не так, и это может вызвать ошибки, если вы ошиблись.


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

17
Кроме того, указатели в наши дни являются 64-битными на большинстве настольных и серверных платформ. Я видел более чем несколько структур в моей карьере, которые вписались бы в 64 бита. Таким образом, вы не всегда можете сказать, что копирование указателя стоит дешевле, чем копирование структуры.
Соломон Медленный

37
Это в основном хороший ответ, но я иногда не согласен с этой частью , очень редко, это то, что вы хотите, но в большинстве случаев это не так - совсем наоборот. Возврат указателя допускает несколько видов нежелательных побочных эффектов и несколько видов отвратительных способов ошибочного владения указателем. В случаях, когда процессорное время не так важно, я предпочитаю вариант копирования, если это вариант, он гораздо менее подвержен ошибкам.
Док Браун

6
Следует отметить, что это действительно относится только к внешним API. Для внутренних функций каждый даже минимально компетентный компилятор последних десятилетий перепишет функцию, которая возвращает большую структуру, чтобы взять указатель в качестве дополнительного аргумента и построить объект непосредственно там. Аргументы immutable vs mutable были сделаны достаточно часто, но я думаю, что мы все можем согласиться с тем, что утверждение о том, что неизменяемые структуры данных почти никогда не являются тем, что вы хотите, не соответствует действительности.
Voo

6
Вы также можете упомянуть сборник противопожарных стен в качестве профи для указателей. В больших программах с широко используемыми заголовками неполные типы с функциями предотвращают необходимость повторной компиляции каждый раз, когда изменяется деталь реализации. Лучшее поведение компиляции на самом деле является побочным эффектом инкапсуляции, который достигается, когда интерфейс и реализация разделены. Возвращая (и передавая, присваивая) по значению, нужна информация о реализации.
Питер - Восстановить Монику

12

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

Для примера fopenвозвращает только одни данные (открытые FILE*) и в случае ошибки выдает код ошибки через errnoпсевдоглобальную переменную. Но, возможно, было бы лучше вернуть a structиз двух членов: FILE*дескриптор и код ошибки (который будет установлен, если дескриптор файла NULL). По историческим причинам это не так (и об ошибках сообщается через errnoглобальный, который сегодня является макросом).

Обратите внимание, что язык Go имеет приятную нотацию для возврата двух (или нескольких) значений.

Также обратите внимание, что в Linux / x86-64 ABI и соглашения о вызовах (см. Страницу x86-psABI ) указывают, что a structиз двух скалярных членов (например, указатель и целое число, или два указателя, или два целых числа) возвращается через два регистра (и это очень эффективно и не идет через память).

Таким образом, в новом C-коде возврат небольшого C-кода structможет быть более читабельным, более ориентированным на потоки и более эффективным.


На самом деле небольшие структуры упакованы в rdx:rax. Таким образом struct foo { int a,b; };, возвращается упакованный в rax(например, с shift / или), и должен быть распакован с shift / mov. Вот пример на Годболт . Но x86 может использовать младшие 32 бита 64-битного регистра для 32-битных операций, не заботясь о старших битах, так что это всегда слишком плохо, но определенно хуже, чем использование 2 регистров большую часть времени для структур с двумя членами.
Питер Кордес

Связанный: bugs.llvm.org/show_bug.cgi?id=34840 std::optional<int> возвращает логическое значение в верхней половине rax, поэтому для его проверки необходима 64-битная константа маски test. Или вы могли бы использовать bt. Но это отстой для вызывающей и сравниваемой вызываемой стороны с использованием dl, которое компиляторы должны делать для «частных» функций. Также связано: libstdc ++ std::optional<T>не копируется тривиально, даже когда T, поэтому всегда возвращается через скрытый указатель: stackoverflow.com/questions/46544019/… . (libc ++'s тривиально копируемый)
Питер Кордес

@PeterCordes: ваши родственные вещи - C ++, а не C
Старынкевич,

Ой, верно. Ну то же самое будет применяться именно к struct { int a; _Bool b; };в C, если абонент хочет проверить логическое значение, поскольку тривиальным-копируемыми C ++ Структуры используют один и тот же ABI , как С.
Питер Кордес

1
Классический примерdiv_t div()
chux - Восстановить Монику

6

Вы на правильном пути

Обе указанные вами причины действительны:

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

Я полагаю, что вернуть структуру FULL со значением NULL будет сложнее или менее эффективно. Это веская причина?

Если у вас есть текстура (например) где-то в памяти, и вы хотите ссылаться на эту текстуру в нескольких местах вашей программы; было бы неразумно делать копию каждый раз, когда вы хотите сослаться на нее. Вместо этого, если вы просто передадите указатель для ссылки на текстуру, ваша программа будет работать намного быстрее.

Самая большая причина - динамическое распределение памяти. Часто, когда программа компилируется, вы не знаете точно, сколько памяти вам нужно для определенных структур данных. Когда это произойдет, объем памяти, который вам нужно использовать, будет определен во время выполнения. Вы можете запросить память, используя 'malloc', а затем освободить ее, когда вы закончите использовать 'free'.

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

И malloc, и free возвращают указатели на места в памяти. Поэтому функции, использующие динамическое распределение памяти, будут возвращать указатели туда, где они создали свои структуры в памяти.

Кроме того, в комментариях я вижу, что есть вопрос, можете ли вы вернуть структуру из функции. Вы действительно можете сделать это. Следующее должно работать:

struct s1 {
   int integer;
};

struct s1 f(struct s1 input){
   struct s1 returnValue = xinput
   return returnValue;
}

int main(void){
   struct s1 a = { 42 };
   struct s1 b= f(a);

   return 0;
}

Как можно не знать, сколько памяти понадобится определенной переменной, если у вас уже определен тип структуры?
yoyo_fun

9
@JenniferAnderson C имеет понятие неполных типов: имя типа может быть объявлено, но еще не определено, поэтому его размер недоступен. Я не могу объявить переменные этого типа, но могу объявить указатели на этот тип, например struct incomplete* foo(void). Таким образом, я могу объявлять функции в заголовке, но определять только структуры в C-файле, что позволяет инкапсуляцию.
Амон

@amon Так вот как на самом деле объявление заголовков функций (прототипов / подписей) перед объявлением о том, как они работают, на самом деле выполняется в C? И то же самое можно сделать со структурами и союзами в C
yoyo_fun

@JenniferAnderson вы объявляете прототипы функций (функции без тел) в заголовочных файлах и затем можете вызывать эти функции в другом коде, не зная тела функций, потому что компилятору просто нужно знать, как расположить аргументы и как принять возвращаемое значение К тому времени, когда вы связываете программу, вам действительно нужно знать определение функции (т. Е. С телом), но вам нужно обработать ее только один раз. Если вы используете непростой тип, ему также необходимо знать структуру этого типа, но указатели часто имеют одинаковый размер, и это не имеет значения для использования прототипа.
simpleuser

6

Что-то вроде a на FILE*самом деле не является указателем на структуру в том, что касается клиентского кода, а является формой непрозрачного идентификатора, связанного с какой-либо другой сущностью, такой как файл. Когда программа вызывает fopen, она, как правило, не заботится ни о каком содержимом возвращаемой структуры - все, о чем она будет заботиться, - это то, что другие функции вроде freadбудут делать с ней все, что им нужно.

Если стандартная библиотека хранит FILE*информацию о, например, текущей позиции чтения в этом файле, вызов freadдолжен иметь возможность обновить эту информацию. Имея freadполучить указатель на FILEделает это легко. Если freadвместо этого получить FILE, он не сможет обновить FILEобъект, удерживаемый вызывающей стороной.


3

Сокрытие информации

В чем преимущество возврата указателя на структуру по сравнению с возвратом всей структуры в операторе возврата функции?

Наиболее распространенным является скрытие информации . С, скажем, не имеет возможности сделать поля structприватными, не говоря уже о методах доступа к ним.

Так что, если вы хотите принудительно запретить разработчикам видеть и манипулировать содержимым объекта pointee, например FILE, единственный способ - не дать им получить доступ к его определению, обрабатывая указатель как непрозрачный, размер которого pointee и определения неизвестны внешнему миру. В этом случае определение FILEбудет видно только тем, кто реализует операции, для которых требуется его определение, например fopen, в то время как общему заголовку будет видна только декларация структуры.

Двоичная совместимость

Сокрытие определения структуры может также помочь обеспечить передышку для сохранения бинарной совместимости в API-интерфейсах dylib. Это позволяет разработчикам библиотеки изменять поля в непрозрачной структуре, не нарушая бинарную совместимость с теми, кто использует библиотеку, поскольку природа их кода должна знать только то, что они могут делать со структурой, а не то, насколько она велика или какие поля в нем есть.

Например, я могу запустить некоторые древние программы, созданные в эпоху Windows 95 сегодня (не всегда идеально, но на удивление многие все еще работают). Скорее всего, в некотором коде этих древних двоичных файлов использовались непрозрачные указатели на структуры, размер и содержание которых изменились с эпохи Windows 95. Тем не менее, программы продолжают работать в новых версиях окон, поскольку они не были открыты для содержимого этих структур. При работе с библиотекой, где важна двоичная совместимость, то, что клиент не подвергает воздействию, обычно может меняться без нарушения обратной совместимости.

КПД

Я полагаю, что вернуть полную структуру, равную NULL, будет сложнее или менее эффективно. Это веская причина?

Как правило, это менее эффективно, если предположить, что тип может практически уместиться и быть распределенным в стеке, если обычно за кулисами не используется гораздо менее обобщенный распределитель памяти, чем malloc, например, уже выделенная память пула распределителя фиксированного размера, а не переменного размера. В данном случае это компромисс безопасности, скорее всего, позволить разработчикам библиотеки поддерживать инварианты (концептуальные гарантии), связанные с FILE.

Это не такая веская причина, по крайней мере, с точки зрения производительности, fopenвозвращать указатель, поскольку единственная причина, по которой он возвращает, NULL- это невозможность открыть файл. Это было бы оптимизацией исключительного сценария в обмен на замедление всех распространенных путей выполнения. В некоторых случаях может быть веская причина повышения производительности, чтобы сделать проекты более прямыми, чтобы они возвращали указатели, чтобы позволить NULLвозвращаться в некоторых постусловиях.

Для файловых операций издержки относительно тривиальны по сравнению с самими файловыми операциями, и ручного управления в fcloseлюбом случае избежать нельзя. Поэтому мы не можем избавить клиента от необходимости освобождения (закрытия) ресурса, предоставляя определение FILEи возвращая его по значению fopenили ожидая значительного увеличения производительности, учитывая относительную стоимость самих файловых операций, чтобы избежать выделения кучи. ,

Горячие точки и исправления

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

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

struct Foo
{
   /* priv_* indicates that you shouldn't tamper with these fields! */
   int priv_internal_field;
   int priv_other_one;
};

struct Foo foo_create(void);
void foo_destroy(struct Foo* foo);
void foo_something(struct Foo* foo);

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

struct Foo
{
   /* priv_* indicates that you shouldn't tamper with these fields! */
   int priv_internal_field;
   int priv_other_one;

   /* reserved for possible future uses (emergency backup plan).
     currently just set to null. */
   void* priv_reserved;
};

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

По моему мнению, скрытие информации и двоичная совместимость, как правило, являются единственной достойной причиной, позволяющей только выделять кучу структур помимо структур переменной длины (что всегда будет требоваться, или, по крайней мере, будет немного неудобно использовать в противном случае, если клиент должен был выделить память в стеке способом VLA для выделения VLS). Даже большие структуры часто дешевле вернуть по значению, если это означает, что программное обеспечение работает намного больше с горячей памятью в стеке. И даже если бы они не были дешевле вернуть по стоимости при создании, можно было бы просто сделать это:

int foo_create(struct Foo* foo);
...
/* In the client code: */
struct Foo foo;
if (foo_create(&foo))
{
    foo_something(&foo);
    foo_destroy(&foo);
}

... инициализировать Fooиз стека без возможности лишнего копирования. Или клиент даже имеет свободу размещения Fooв куче, если он хочет по какой-то причине.

Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.