Чому багато функцій, які повертають структури в С, насправді повертають покажчики на структури?


49

Яка перевага повернення вказівника на структуру, на відміну від повернення всієї структури в returnоператорі функції?

Я говорю про такі функції, як fopenта інші функції низького рівня, але, ймовірно, є функції вищого рівня, які також повертають покажчики до структур.

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

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

Повернути повну структуру, яка NULLбуде важче, я вважаю, або менш ефективною. Це поважна причина?


9
@ JohnR.Strohm Я спробував це, і він насправді працює. Функція може повернути структуру .... Отже, яка причина не робиться?
yoyo_fun

27
Попередня стандартизація C не дозволила скопіювати структури або передати їх за значенням. Стандартна бібліотека C має безліч вакансій тієї епохи, які не були б написані таким чином сьогодні, наприклад, до моменту усунення вкрай неправильно розробленої gets()функції до C11 пішло не зовсім . Деякі програмісти все ще мають відраза до копіювання структур, старі звички важко вмирають.
амон

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

3
Повернення за посиланням - лише розумний дефолт, коли ви збираєте сміття.
Ідан Ар'є

6
@ 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. Для внутрішніх функцій кожен навіть незначно грамотний компілятор останніх десятиліть перепише функцію, яка повертає велику структуру, щоб взяти покажчик як додатковий аргумент і сконструювати об’єкт прямо там. Аргументи незмінних проти змінних були зроблені досить часто, але я думаю, що ми всі можемо погодитися, що твердження про те, що незмінні структури даних майже ніколи не потрібні, не відповідає дійсності.
Voo

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

12

Окрім інших відповідей, іноді варто повернути мале struct значення. Наприклад, можна повернути пару одних даних і деякий код помилки (або успіху), пов'язаний з ним.

Для прикладу, fopenповертає лише одні дані (відкриті FILE*), а в разі помилки подає код помилки через errnoпсевдо глобальну змінну. Але було б, можливо, краще повернути structдва члена: FILE*ручку та код помилки (який буде встановлено, якщо обрати файл NULL). З історичних причин це не так (і про помилки повідомляється через errnoглобальний, який сьогодні є макросом).

Зауважте, що мова Go має гарне позначення повернення двох (або кількох) значень.

Зауважте також, що на Linux / x86-64 конвенції ABI та виклики (див. Сторінку x86-psABI ) вказує, що structз двох реєстрів повертається два скалярні члени (наприклад, покажчик і ціле число, або два покажчики, або два цілі числа) (і це дуже ефективно і не проходить через пам'ять).

Тож у новому коді С повернення невеликого C structможе бути читабельніше, зручнішим для потоків та ефективнішим.


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

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

@PeterCordes: ваші споріднені речі - це C ++, а не C
Basile Starynkevitch

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

1
Класичний прикладdiv_t div()
chux

6

Ви на правильному шляху

Обидві причини, про які ви згадали, є дійсними:

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

Повернути ПОВНУ структуру, яка є NULL, було б складніше, я вважаю, або менш ефективною. Це поважна причина?

Якщо у вас є текстура (наприклад) десь у пам'яті, і ви хочете вказати цю текстуру в декількох місцях вашої програми; не було б розумно робити копію кожного разу, коли ви хочете посилатися на неї. Натомість, якщо ви просто пройдете навколо вказівника для посилання на текстуру, ваша програма запуститься набагато швидше.

Найбільшою причиною є динамічне розподіл пам'яті. Часто, коли компілюється програма, ви не впевнені, скільки саме пам'яті потрібно для певних структур даних. Коли це станеться, обсяг пам'яті, який потрібно використовувати, визначатиметься під час виконання. Ви можете запитати пам'ять за допомогою "malloc", а потім звільнити її, коли закінчите використовувати "безкоштовно".

Хорошим прикладом цього є читання з файлу, який вказав користувач. У цьому випадку ви не маєте поняття, наскільки може бути великий файл при складанні програми. Ви можете лише зрозуміти, скільки пам'яті вам потрібно, коли програма насправді працює.

Як вказівники на malloc, так і на безкоштовне повернення до пам'яті. Таким чином, функції, що використовують динамічне розподілення пам'яті, повернуть покажчики туди, де вони створили свої структури в пам'яті.

Також у коментарях я бачу, що виникає питання, чи можна повернути структуру з функції. Ви дійсно можете це зробити. Слід працювати:

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? І те ж саме можна зробити структурам та профспілкам в С
yoyo_fun

@JenniferAnderson ви оголошуєте прототипи функцій (функції без тіл) у файлах заголовків, а потім можете викликати ці функції в іншому коді, не знаючи суті функцій, тому що компілятору просто потрібно знати, як упорядкувати аргументи та як прийняти повернене значення. До моменту підключення програми ви фактично повинні знати визначення функції (тобто з тілом), але обробляти її потрібно лише один раз. Якщо ви використовуєте не простий тип, він також повинен знати структуру цього типу, але вказівники часто однакового розміру, і це не має значення для використання прототипу.
simpleuser

6

Щось на зразок FILE*не є насправді вказівником на структуру, що стосується коду клієнта, а натомість є формою непрозорого ідентифікатора, асоційованого з якоюсь іншою сутністю, як файл. Коли програма дзвонить fopen, вона, як правило, не піклується про будь-який вміст повернутої структури - все, що її буде хвилювати, - це те, що інші функції, як, наприклад, freadбудуть робити все, що потрібно робити з нею.

Якщо стандартна бібліотека зберігає FILE*інформацію про, наприклад, поточну позицію читання в цьому файлі, потрібно freadбуде закликати оновити цю інформацію. Отримавши freadвказівник на FILEматриці, це легко. Якщо б freadзамість цього було отримано знак " FILE," оновлення FILEоб'єкта, що належить абоненту , не матиме жодного способу .


3

Приховування інформації

Яка перевага повернення вказівника на структуру на відміну від повернення всієї структури в операторі повернення функції?

Найпоширеніший - це приховування інформації . Скажімо, C не має можливості робити поля structприватними, не кажучи вже про надання методів доступу до них.

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

Бінарна сумісність

Приховування визначення структури також може допомогти забезпечити приміщення для дихання для збереження бінарної сумісності в API дилібу. Це дозволяє реалізаторам бібліотеки змінювати поля в непрозорій структурі, не порушуючи бінарної сумісності з тими, хто використовує бібліотеку, оскільки природа їх коду лише повинна знати, що вони можуть робити зі структурою, а не наскільки вона велика або які поля Це має.

Як приклад, я фактично можу запустити деякі старовинні програми, побудовані в епоху Windows 95 сьогодні (не завжди ідеально, але на диво багато хто все ще працює). Цілком ймовірно, що частина коду для цих стародавніх двійкових файлів використовувала непрозорі покажчики на структури, розміри та вміст яких змінився з епохи Windows 95. Проте програми продовжують працювати в нових версіях Windows, оскільки вони не піддавалися впливу вмісту цих структур. Під час роботи над бібліотекою, де важлива бінарна сумісність, те, що клієнт не піддається впливу, як правило, дозволяється змінювати, не порушуючи зворотної сумісності.

Ефективність

Повернути повну структуру, яка є NULL, було б важче або, напевно, менш ефективним. Це поважна причина?

Зазвичай це менш ефективно, якщо припустити, що тип може практично підходити і бути розподіленим на стеці, якщо, як правило, не використовується набагато менш узагальнений розподільник пам'яті, який використовується за кадром, ніж malloc, наприклад, фіксований розмір, а не змінний розмір об'єднаної пам'яті, об'єднаний у пам'ять. У цьому випадку, найімовірніше, це безпека, що дозволяє розробникам бібліотек підтримувати інваріанти (концептуальні гарантії), пов'язані з цим FILE.

Це не така вагома причина, принаймні з точки зору продуктивності, щоб fopenповернути покажчик, оскільки єдина причина, по якій він повертається, NULL- це невдача відкрити файл. Це було б оптимізацією виняткового сценарію в обмін на уповільнення всіх загальноприйнятих випадків виконання. У деяких випадках може бути поважна причина продуктивності, щоб зробити конструкції простішими, щоб змусити їх повертати покажчики, щоб вони NULLмогли повернутись за деякими пост-умовами.

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

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

В інших випадках, проте, я перепрофілював багато марнотратнього коду С у застарілих кодових базах з гарячими точками 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.