C API-дизайн: Кто должен выделять?

Каков надлежащий / предпочтительный способ выделения памяти в C API?

Сначала я вижу два варианта:

1) Позвольте вызывающему абоненту выполнять всю (внешнюю) обработку памяти:

myStruct *s = malloc(sizeof(s)); myStruct_init(s); myStruct_foo(s); myStruct_destroy(s); free(s); 

Функции _init и _destroy необходимы, так как внутри может быть выделено больше памяти, и ее нужно где-то обрабатывать.

Это имеет недостаток в том, что он длиннее, но в некоторых случаях также можно исключить malloc (например, ему может быть передана структура, распределенная по стеку:

 int bar() { myStruct s; myStruct_init(&s); myStruct_foo(&s); myStruct_destroy(&s); } 

Кроме того, для звонящего необходимо знать размер структуры.

2) Скрыть malloc s в _init и free s в _destroy .

Преимущества: более короткий код, поскольку функции все равно будут вызваны. Полностью непрозрачные структуры.

Недостатки: нельзя передать структуру, выделенную по-другому.

 myStruct *s = myStruct_init(); myStruct_foo(s); myStruct_destroy(foo); 

В настоящее время я склоняюсь к первому делу; опять же, я не знаю о C API-дизайне.

Моим любимым примером хорошо продуманного C API является GTK +, который использует метод # 2, который вы описываете.

Хотя еще одно преимущество вашего метода №1 состоит не только в том, что вы могли бы выделить объект в стеке, но и в том, что вы могли бы повторно использовать один и тот же экземпляр несколько раз. Если это не будет распространенным вариантом использования, то простота # 2, вероятно, является преимуществом.

Конечно, это только мое мнение 🙂

Метод номер 2 каждый раз.

Зачем? потому что с помощью метода номер 1 вы должны пропустить информацию о реализации для вызывающего. Вызывающий должен знать, по крайней мере, насколько велика структура. Вы не можете изменить внутреннюю реализацию объекта, не перекомпилируя код, который его использует.

Другим недостатком № 2 является то, что вызывающий абонент не имеет контроля над распределением вещей. Это можно обойти, предоставив API для клиента, чтобы зарегистрировать свои собственные функции распределения / освобождения (например, SDL), но даже это может быть недостаточно мелкозернистым.

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

Преимущество № 2 состоит в том, что он позволяет вам выставлять свой тип данных строго как непрозрачный указатель (т. Е. Объявлять структуру, но не определять ее, а последовательно использовать указатели). Затем вы можете изменить определение структуры по своему усмотрению в будущих версиях вашей библиотеки, в то время как клиенты остаются совместимыми на двоичном уровне. С # 1 вы должны сделать это, потребовав, чтобы клиент каким-то образом указал версию внутри структуры (например, все эти поля cbSize в Win32 API), а затем вручную написал код, который может обрабатывать как старые, так и более новые версии структуры оставаться бинарно-совместимым по мере развития вашей библиотеки.

В общем, если ваши структуры являются прозрачными данными, которые не будут меняться с будущей незначительной ревизией библиотеки, я бы пошел с №1. Если это более или менее сложный объект данных, и вы хотите, чтобы полная инкапсуляция была надежной для будущего развития, перейдите к # 2.

Почему бы не обеспечить оба, чтобы получить лучшее из обоих миров?

Используйте функции _init и _terminate, чтобы использовать метод # 1 (или любое другое название, которое вы считаете нужным).

Используйте дополнительные функции _create и _destroy для динамического выделения. Поскольку _init и _terminate уже существуют, это эффективно сводится к:

 myStruct *myStruct_create () { myStruct *s = malloc(sizeof(*s)); if (s) { myStruct_init(s); } return (s); } void myStruct_destroy (myStruct *s) { myStruct_terminate(s); free(s); } 

Если вы хотите, чтобы он был непрозрачным, сделайте _init и _terminate static и не выставляйте их в API, только предоставляйте _create и _destroy. Если вам нужны другие распределения, например, с заданным обратным вызовом, предоставить для этого другой набор функций, например _createcalled, _destroycalled.

Важно следить за распределением, но вы все равно должны это делать. Вы всегда должны использовать аналог используемого распределителя для освобождения.

Оба функционально эквивалентны. Но, на мой взгляд, метод №2 проще в использовании. Несколько причин для выбора 2 более 1:

  1. Это более интуитивно понятно. Зачем мне нужно free звонить на объект после того, как я (по-видимому) его уничтожил, используя myStruct_Destroy .

  2. Скрывает детали myStruct от пользователя. Ему не нужно беспокоиться о его размере и т. Д.

  3. В методе №2 myStruct_init не нужно беспокоиться о начальном состоянии объекта.

  4. Вам не нужно беспокоиться о утечке памяти от пользователя, забывающего звонить free .

Однако, если ваша реализация API будет отправлена ​​как отдельная разделяемая библиотека, метод №2 является обязательным. Чтобы изолировать ваш модуль от любого несоответствия в реализациях malloc / new и free / delete в версиях компилятора, вы должны сохранить выделение памяти и де-распределение для себя. Обратите внимание, что это более верно для C ++, чем для C.

Проблема, с которой я сталкиваюсь в первом методе, заключается не столько в том, что она больше для вызывающего, а в том, что api теперь нарушена наручниками при расширении объема памяти, которое она использует, потому что она не знает, как память получил. Вызывающий не всегда знает заранее, сколько памяти ему понадобится (представьте, если вы пытаетесь реализовать вектор).

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

Что касается того, какой метод является правильной апи-дизайном, то это делается в стандартной библиотеке C. strdup () и stdio использует второй метод, в то время как sprintf и strcat используют первый метод. Лично я предпочитаю второй метод (или третий), если 1) я не знаю, что мне никогда не понадобится realloc, и 2) Я ожидаю, что срок службы моих объектов будет коротким, и, таким образом, использование стека очень убедительно

edit: На самом деле есть 1 другой вариант, и он плохой, с известным прецедентом. Вы можете сделать это так, как strtok () делает это со статикой. Нехорошо, только что упомянуто для полноты.

Оба способа в порядке, я, как правило, делаю первый путь, так как многие CI делают для встроенных систем, и вся память – это либо крошечные переменные в стеке, либо статически распределенные. Таким образом, не может быть недостатка в памяти, либо у вас достаточно в начале, либо с самого начала. Полезно знать, когда у вас есть 2K Ram 🙂 Итак, все мои библиотеки похожи на # 1, где предполагается, что память будет выделена.

Но это краевой пример развития C.

Сказав это, я, вероятно, поеду с №1. Возможно, использование init и finalize / dispose (а не уничтожение) для имен.

Это может дать некоторый элемент рефлексии:

случай # 1 имитирует схему распределения памяти C ++, с более или менее одинаковыми преимуществами:

  • простое распределение временных рядов на стеке (или в статических массивах или такое, чтобы написать собственный распределитель структуры, заменяющий malloc).
  • легко избавиться от памяти, если что-то пойдет не так в init

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

Смешанный API между случаем № 1 и случаем № 2 также распространен: есть поле, используемое для передачи указателем на некоторую уже инициализированную структуру, если оно равно null, оно выделено (и указатель всегда возвращается). С таким API бесплатный обычно является обязанностью вызывающего абонента, даже если init выполняет выделение.

В большинстве случаев я, вероятно, поеду на случай №1.

Оба приемлемы – между ними есть компромиссы, как вы уже отмечали.

Есть большие реальные примеры мира – как говорит Дин Хардинг , GTK + использует второй метод; OpenSSL – пример, который использует первый.

Я бы пошел (1) с одним простым расширением, т. _init Чтобы ваша _init функция всегда возвращала указатель на объект. После этого инициализация указателя может быть просто прочитана:

 myStruct *s = myStruct_init(malloc(sizeof(myStruct))); 

Поскольку вы можете видеть правую сторону, тогда только ссылка на тип, а не на переменную больше. Простой макрос затем дает вам (2) по крайней мере частично

 #define NEW(T) (T ## _init(malloc(sizeof(T)))) 

и инициализация указателя

 myStruct *s = NEW(myStruct); 

См. Ваш метод №2.

 myStruct *s = myStruct_init(); myStruct_foo(s); myStruct_destroy(s); 

Теперь посмотрим, myStruct_init() ли для myStruct_init() возвращать некоторый код ошибки по разным причинам, а затем отпустить этот путь.

 myStruct *s; int ret = myStruct_init(&s); // int myStruct_init(myStruct **s); myStruct_foo(s); myStruct_destroy(s);