Сокрытие информации
В чем преимущество возврата указателя на структуру по сравнению с возвратом всей структуры в операторе возврата функции?
Наиболее распространенным является скрытие информации . С, скажем, не имеет возможности сделать поля 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
в куче, если он хочет по какой-то причине.