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 в _init и free в _destroy.

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

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

myStruct *s = myStruct_init();

myStruct_foo(s);

myStruct_destroy(foo);

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

Ответ 1

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

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

Конечно, это просто мое мнение:)

Ответ 2

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

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

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

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

Ответ 3

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

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

Ответ 4

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

Используйте функции _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.

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

Ответ 5

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

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

  • Скрывает информацию о пользователе myStruct от пользователя. Ему не нужно беспокоиться об этом размере и т.д.

  • В методе # 2 myStruct_init не нужно беспокоиться об исходном состоянии объекта.

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

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

Ответ 6

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

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

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

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

Ответ 7

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

Но это краевой случай разработки C.

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

Ответ 8

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

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

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

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

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

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

Ответ 9

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

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

Ответ 10

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

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

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

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

и ваша инициализация указателя читает

myStruct *s = NEW(myStruct);

Ответ 11

См. ваш метод # 2 говорит

myStruct *s = myStruct_init();

myStruct_foo(s);

myStruct_destroy(s);

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

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

myStruct_foo(s);

myStruct_destroy(s);