Чи слід ініціалізувати структури C через параметр або значення повернення? [зачинено]


33

Компанія, в якій я працюю, ініціалізує всі їх структури даних за допомогою функції ініціалізації:

//the structure
typedef struct{
  int a,b,c;  
} Foo;

//the initialize function
InitializeFoo(Foo* const foo){
   foo->a = x; //derived here based on other data
   foo->b = y; //derived here based on other data
   foo->c = z; //derived here based on other data
}

//initializing the structure  
Foo foo;
InitializeFoo(&foo);

Я отримав деякий поштовх назад, намагаючись ініціалізувати свої структури так:

//the structure
typedef struct{
  int a,b,c;  
} Foo;

//the initialize function
Foo ConstructFoo(int a, int b, int c){
   Foo foo;
   foo.a = a; //part of parameter input (inputs derived outside of function)
   foo.b = b; //part of parameter input (inputs derived outside of function)
   foo.c = c; //part of parameter input (inputs derived outside of function)
   return foo;
}

//initialize (or construct) the structure
Foo foo = ConstructFoo(x,y,z);

Чи є перевага один перед іншим?
Що робити, і як би я обґрунтував це як кращу практику?


4
@gnat Це явне запитання щодо ініціалізації структури. Цей потік втілює в собі одне й те саме обґрунтування, яке я хотів би бачити як застосований для цього конкретного дизайнерського рішення.
Тревор Хікі

2
@Jefffrey Ми в C, тому насправді не можемо мати методів. Це не завжди є прямим набором значень. Іноді ініціалізація структури - це отримання значень (якось) і виконує певну логіку для ініціалізації структури.
Тревор Хікі

1
@JacquesB Я отримав "Кожен компонент, який ви будуєте, буде відрізнятися від інших. Існує функція Initialize (), яка використовується в іншому місці для структури. Технічно кажучи, називаючи це конструктором, вводить в оману".
Тревор Хікі

1
@TrevorHickey InitializeFoo()- це конструктор. Єдина відмінність від конструктора C ++ полягає в тому, що thisвказівник передається явно, а не неявно. Скомпільований код InitializeFoo()і відповідний C ++ Foo::Foo()точно такий же.
cmaster

2
Кращий варіант: припиніть використовувати C над C ++. Автовін.
Томас Едінг

Відповіді:


25

У другому підході у вас ніколи не буде напівініціалізованого Foo. Розмістити всю конструкцію в одному місці здається більш розумним і очевидним місцем.

Але ... 1-й спосіб не такий вже й поганий, і його часто використовують у багатьох областях (є навіть обговорення найкращого способу введення залежності, або введення властивості, як ваш перший спосіб, або введення конструктора, як 2-го) . Ні це не так.

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


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

4
@TrevorHickey: Насправді я б сказав, що між наведеними прикладами є дві ключові відмінності - (1) В одному з функцій передається вказівник на структуру для ініціалізації, а в іншому вона повертає ініціалізовану структуру; (2) В одному параметри ініціалізації передаються функції, а в іншому - неявними. Ви наче запитуєте більше про (2), але відповіді тут зосереджені на (1). Ви можете уточнити, що - я підозрюю, що більшість людей рекомендують гібрид двох із використанням явних параметрів і вказівника:void SetupFoo(Foo *out, int a, int b, int c)
psmears

1
Як би перший підхід призвів до структури, наполовину ініціалізованої Foo? Перший підхід також виконує всю ініціалізацію в одному місці. (Або ви роздумуєте про ООН инициализируется - Fooструктуру , щоб бути «наполовину инициализирован»?)
jamesdlin

1
@jamesdlin у випадках, коли створено Foo, а InitialiseFoo випадково пропущено. Це було просто фігурою мови, щоб описати двофазну ініціалізацію, не набираючи довгого опису. Я вважав, що досвідчені розробники типу людей зрозуміють.
gbjbaanb

22

Обидва підходи поєднують код ініціалізації в єдиний виклик функції. Все йде нормально.

Однак є два питання з другим підходом:

  1. Другий фактично не конструює отриманий об'єкт, він ініціалізує інший об’єкт на стеку, який потім копіюється на кінцевий об'єкт. Ось чому я бачив би другий підхід як трохи неповноцінний. Отриманий вами відштовхувач, ймовірно, пов'язаний із цією стороною копією.

    Це ще гірше, коли ви отримуєте заняття Derived від Foo(Структур в основному використовується для орієнтації об'єкта в C): При другому підході, функція ConstructDerived()буде називати ConstructFoo(), скопіювати отриманий тимчасовий Fooоб'єкт протягом суперкласу слот Derivedоб'єкта; закінчити ініціалізацію Derivedоб’єкта; тільки щоб отриманий об'єкт знову був скопійований після повернення. Додайте третій шар, і вся справа стає абсолютно смішною.

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


Нарешті, не всі structsє повноцінними класами. Деякі structsфактично просто зв'язують купу змінних разом, без будь-яких внутрішніх обмежень значень цих змінних. typedef struct Point { int x, y; } Point;був би хорошим прикладом цього. Для цих функцій використання ініціалізатора здається непосильним. У цих випадках складний буквальний синтаксис може бути зручним (це C99):

Point = { .x = 7, .y = 9 };

або

Point foo(...) {
    //other stuff

    return (Point){ .x = n, .y = n*n };
}

5
Я не думав, що копії будуть проблемою через en.wikipedia.org/wiki/Copy_elision
Тревор Хікі

5
Те, що компілятор може усунути копію, не полегшує той факт, що ви записали копію. У C написання зайвих операцій та покладання на компілятор для їх виправлення вважається поганою стилістикою. Це відрізняється від C ++, де люди пишаються тим, що можуть довести, що компілятор теоретично може видалити всю шару, що залишилася від їх вкладених шаблонів. На C люди намагаються написати саме той код, який має виконати машина. Як би там не було, питання про важкодоступні адреси залишається, копія elision не може вам там допомогти.
cmaster

3
Кожен, хто використовує компілятор, повинен очікувати, що код, який вони пишуть, буде перетворений компілятором. Якщо вони не мають апаратного інтерпретатора C, код, який вони пишуть, не буде кодом, який вони виконують, навіть якщо легко повірити в інше. Якщо вони зрозуміють свій компілятор, вони зрозуміють elision, і це не відрізняється від того, щоб int x = 3;не зберігати рядок xу двійковій. Адреса та бали спадкування хороші; передбачуваний збій елізону нерозумний.
Якк

@Yakk: Історично C було винайдено, щоб слугувати формою високомовної мови складання для програмування систем. З роками з того часу її ідентичність стає все більш мутною. Деякі люди хочуть, щоб це була оптимізована мова додатків, але оскільки не з’явилась краща форма мови високого рівня монтажу, C все ще потрібен для виконання цієї останньої ролі. Я не бачу нічого поганого в думці, що добре написаний програмний код повинен вести себе принаймні пристойно навіть при компіляції з мінімальними оптимізаціями, хоча для того, щоб це справді працювало, потрібно, щоб С додав деякі речі, яких йому давно не вистачало.
supercat

@Yakk: Маючи директиви, наприклад, які б сказали компілятору "Наступні змінні можуть безпечно зберігатися в регістрах під час наступного розтягування коду", а також засоби блок-копіювання типу, крім того, unsigned charщо дозволить оптимізувати, для яких Суворе правило згладжування було б недостатнім, водночас ясніше очікувань програміста.
supercat

1

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

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

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


3
До останнього моменту: у C ++ немає нічого, що може зупинити функцію зберігання копії вказівника, переданого як посилання, функція може просто приймати адресу об'єкта. Також вільно використовувати посилання для побудови іншого об'єкта, який містить цю посилання (не створено голий покажчик). Тим не менш, або копія вказівника, або посилання на об'єкт можуть переживати об'єкт, на який вони вказують, створюючи звисаючий покажчик / посилання. Тож питання щодо довідкової безпеки є досить приглушеним.
cmaster

@cmaster: На платформах, які повертають структури, передаючи покажчик у тимчасове сховище, компілятори C не надають функціям, що викликаються, жодним способом доступу до адреси цього сховища. У C ++ можна отримати адресу змінної, передану за посиланням, але, якщо абонент не гарантує термін служби переданого елемента (у такому випадку це, як правило, пройшов би вказівник), може призвести Невизначена поведінка.
supercat

1

Один аргумент на користь стилю "вихідний параметр" полягає в тому, що він дозволяє функції повертати код помилки.

struct MyStruct {
    int x;
    char *y;
    // ...
};

int MyStruct_init(struct MyStruct *out) {
    // ...
    char *c = malloc(n);
    if (!c) {
        return -1;
    }
    out->y = c;
    return 0;  // Success!
}

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


1
Хоча можна було просто встановити errno.
Дедуплікатор

0

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

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


Downvoter, хочете пояснити? Щось я сказав фактично невірно?
jamesdlin
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.