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