Должен ли я инициализировать структуры 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()абсолютно одинаковы.
Мастер
2
Лучший вариант: прекратить использование C над C ++. Autowin.
Томас Эдинг

Ответы:

25

Во втором подходе у вас никогда не будет полуинициализированного Foo. Размещение всей конструкции в одном месте кажется более разумным и очевидным.

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

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

gbjbaanb
источник
Хорошо, кажется разумным. У меня сложилось впечатление, что инициализация объекта без возможности увидеть, какой тип ввода инициализирует его, приведет к путанице. Я пытался следовать концепции ввода / вывода данных для создания предсказуемого и тестируемого кода. Похоже, что сделать это другим способом увеличило связь, так как исходный файл моей структуры нуждался в дополнительных зависимостях для выполнения инициализации. Вы правы, хотя, в том, что я не хочу раскачивать лодку, если один путь не предпочтительнее другого.
Тревор Хикки
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 };
}
cmaster
источник
5
Я не думал, что копии будут проблемой из-за en.wikipedia.org/wiki/Copy_elision
Тревор Хики
5
То, что компилятор может исключить копию, не облегчает тот факт, что вы записали копию. В C написание лишних операций и использование компилятора для их исправления считается плохим стилем. Это отличается от C ++, где люди гордятся тем, что они могут доказать, что компилятор теоретически может удалить всю пустоту, оставленную их вложенными шаблонами. В Си люди пытаются написать именно тот код, который машина должна выполнить. В любом случае, вопрос о недоступных адресах остается, копирование не поможет вам в этом.
мастер
3
Любой, кто использует компилятор, должен ожидать, что код, который они пишут, будет преобразован компилятором. Если они не используют аппаратный интерпретатор C, код, который они пишут, не будет кодом, который они выполняют, даже если в это легко поверить. Если они понимают свой компилятор, они поймут elision, и это ничем не отличается от того, как int x = 3;не хранить строку xв двоичном файле. Адрес и точки наследования хороши; Предполагаемая неудача выбывания глупа.
Якк
@Yakk: Исторически C был изобретен как форма языка ассемблера высокого уровня для системного программирования. С тех пор его личность становилась все более и более мрачной. Некоторые люди хотят, чтобы это был оптимизированный язык приложений, но, поскольку не появилось лучшей формы языка ассемблера высокого уровня, C по-прежнему необходим для выполнения этой последней роли. Я не вижу ничего плохого в идее, что хорошо написанный программный код должен вести себя по крайней мере прилично, даже если он скомпилирован с минимальной оптимизацией, хотя для того, чтобы это действительно работало, потребовалось бы, чтобы C добавил некоторые вещи, которых ему давно не хватало.
суперкат
@Yakk: Наличие директив, например, которые сообщали бы компилятору: «Следующие переменные могут безопасно храниться в регистрах во время следующего фрагмента кода», а также средство для блочного копирования типа, отличного от того, unsigned charкоторый позволял бы оптимизировать, для которого Строгое правило псевдонимов было бы недостаточно, одновременно делая ожидания программистов более ясными.
суперкат
1

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

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

Жаль, что C не дает возможности сказать, что функции запрещено хранить копию переданного указателя (семантика аналогична ссылке на C ++), поскольку передача такого ограниченного указателя принесет прямые преимущества в производительности передачи указатель на существующий ранее объект, но в то же время избегайте семантических затрат, связанных с требованием, чтобы компилятор считал адрес переменной «открытым».

Supercat
источник
3
К вашему последнему пункту: в C ++ нет ничего, что могло бы помешать функции хранить копию указателя, переданного в качестве ссылки, функция может просто взять адрес объекта. Также можно свободно использовать ссылку для создания другого объекта, который содержит эту ссылку (не создается голый указатель). Тем не менее, либо копия указателя, либо ссылка в объекте могут пережить объект, на который они указывают, создавая висячий указатель / ссылку. Таким образом, пункт о безопасности ссылок довольно немой.
Мастер
@cmaster: На платформах, которые возвращают структуры путем передачи указателя во временное хранилище, компиляторы C не предоставляют вызываемым функциям никакого доступа к адресу этого хранилища. В C ++ возможно получить адрес переменной, переданной по ссылке, но если вызывающая сторона не гарантирует время жизни переданного элемента (в этом случае он обычно передал бы указатель), вероятно, возникнет неопределенное поведение.
суперкат
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!
}

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

Виктор Даль
источник
1
Хотя можно просто установить errno.
Дедупликатор
0

Я предполагаю, что вы сосредоточены на инициализации через выходной параметр, а не на инициализации через return, а не на несоответствии в том, как предоставляются аргументы конструкции.

Обратите внимание, что первый подход может позволить Fooбыть непрозрачным (хотя и не так, как вы используете его в настоящее время), и это обычно желательно для долгосрочной ремонтопригодности. Например, вы можете рассмотреть функцию, которая выделяет непрозрачную Fooструктуру без ее инициализации. Или, возможно, вам нужно повторно инициализировать Fooструктуру, которая ранее была инициализирована с другими значениями.

jamesdlin
источник
Downvoter, хочешь объяснить? Что-то я сказал на самом деле неправильно?
Джеймсдлин