В C, как бы я выбрал, возвращать ли структуру или указатель на структуру?


работа над моей мышцей C в последнее время и просмотр многих библиотек, с которыми я работал, безусловно, дали мне хорошее представление о том, что такое хорошая практика. Одна вещь, которую я не видел-это функция, которая возвращает структуру:

something_t make_something() { ... }

из того, что я впитал это "правильный" способ сделать это:

something_t *make_something() { ... }
void destroy_something(something_t *object) { ... }

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

кроме того, как этот параметр сравнивать?

void make_something(something_t *object)
6 52

6 ответов:

, когда something_t мало (читай: копирование это примерно так же дешево, как копирование указателя), и вы хотите, чтобы он был выделен стеком по умолчанию:

something_t make_something(void);

something_t stack_thing = make_something();

something_t *heap_thing = malloc(sizeof *heap_thing);
*heap_thing = make_something();

, когда something_t большой или вы хотите быть в куче:

something_t *make_something(void);

something_t *heap_thing = make_something();

независимо от размера something_t, и если вам все равно, где он расположен:

void make_something(something_t *);

something_t stack_thing;
make_something(&stack_thing);

something_t *heap_thing = malloc(sizeof *heap_thing);
make_something(heap_thing);

это почти всегда о стабильности ABI. Двоичная стабильность между версиями библиотеки. В тех случаях, когда это не так, иногда речь идет о динамическом размере структур. Редко речь идет о чрезвычайно больших structs или производительность.


это чрезвычайно редко, что выделение struct в куче и возвращая его почти так же быстро, как возвращая его по значению. Элемент struct должно быть огромным.

действительно, скорость не является причиной за методом 2, возврат по указателю, вместо возврата по значению.

Метод 2 существует для стабильности ABI. Если у вас есть struct и ваша следующая версия библиотеки добавляет к ней еще 20 полей, потребители вашей предыдущей версии библиотеки совместимы на уровне двоичного кода если им передают предварительно сконструированные указатели. Дополнительные данные за пределами конца struct Они знают о чем-то, о чем они не должны знать.

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

Метод 2 также позволяет скрыть дополнительные данные как до, так и после возвращаемого указателя (какие версии, добавляющие данные в конец структуры, являются вариантом). Вы можете закончить структуру массивом переменного размера или добавить указатель с некоторыми дополнительными данными, или оба.

если вы хотите stack-allocated structs в стабильном ABI, почти все функции, которые говорят с struct необходимо передать информацию о версии.

так

something_t make_something(unsigned library_version) { ... }

здесь library_version используется библиотекой для определения того, какая версия something_t ожидается возвращение и это изменяет, сколько стека он манипулирует. Это невозможно с помощью стандартного C, но

void make_something(something_t* here) { ... }

есть. В этом случае something_t может есть version поле как его первый элемент (или поле размера), и вам потребуется, чтобы он был заполнен до вызова make_something.

другой код библиотеки принимает something_t затем запросит version поле для определения какой версии something_t они работают.

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

думаю, что void make_something(something_t *object) является наиболее распространенным способом использования структур в C. Вы оставляете выделение для вызывающего абонента. Это эффективно, но не красиво.

однако объектно-ориентированные программы на C используют something_t *make_something() так как они построены с понятием непрозрачного типа, которая заставляет вас использовать указатели. Является ли возвращенный указатель на динамическую память или что-то другое, зависит от реализации. OO с непрозрачным типом часто является одним из самых элегантных и лучших способов разработки более сложных программ на C, но, к сожалению, немногие программисты на C знают/заботятся об этом.

некоторые плюсы первого подхода:

  • меньше кода писать.
  • более идиоматично для случая использования возврата нескольких значений.
  • работает на системах, которые не имеют динамическое распределение.
  • вероятно, быстрее для небольших или небольших объектов.
  • нет утечки памяти из-за забывая free.

некоторые минусы:

  • если объект большой (скажем, мегабайт) , может вызвать переполнение стека, или может быть медленным, если компиляторы не оптимизируют его наилучшим образом.
  • может удивить людей, которые узнали C в 1970-х годах, когда это было невозможно, и не в курсе.
  • не работает с объектами, которые содержат указатель на часть себя.

Я несколько удивлен.

разница в том, что Пример 1 создает структуру в стеке, Пример 2 создает ее в куче. В коде C или C++, который фактически является C, это идиоматично и удобно создавать большинство объектов в куче. В C++ этого нет, в основном они идут в стек. Причина в том, что если вы создаете объект в стеке, деструктор вызывается автоматически, если вы создаете его в куче, он должен быть вызван explicitly.So это намного проще обеспечить там нет утечек памяти и для обработки исключений все идет на стеке. В C деструктор должен быть вызван явно в любом случае, и нет понятия специальной функции деструктора (у вас есть деструкторы, конечно, но они просто обычные функции с именами, такими как destroy_myobject()).

теперь исключение в C++ предназначено для низкоуровневых объектов-контейнеров, например векторов, деревьев, хэш-карт и т. д. Они сохраняют элементы кучи, и у них есть деструкторы. Сейчас самая память-тяжелая объекты состоят из нескольких непосредственных элементов данных, дающих размеры, идентификаторы, теги и т. д., а затем остальную информацию в структурах STL, возможно, вектор пиксельных данных или карту английских пар слово / значение. Таким образом, большая часть данных фактически находится в куче, даже в C++.

а современный C++ устроен так, что этот паттерн

class big
{
    std::vector<double> observations; // thousands of observations
    int station_x;                    // a bit of data associated with them
    int station_y; 
    std::string station_name; 
}  

big retrieveobservations(int a, int b, int c)
{
    big answer;
    //  lots of code to fill in the structure here

    return answer;
}

void high_level()
{
   big myobservations = retriveobservations(1, 2, 3);
}

будет компилироваться в довольно эффективный код. Большой элемент наблюдения не будет генерировать ненужные копии макета.

В отличие от некоторых других языков (например, Python), C не имеет понятия a кортежа!--7-->. Например, в Python допустимо следующее:

def foo():
    return 1,2

x,y = foo()
print x, y

функции foo возвращает два значения в виде кортежа, которые присваиваются x и y.

поскольку C не имеет понятия кортежа, неудобно возвращать несколько значений из функции. Один из способов обойти это-определить структуру для хранения значений, а затем вернуть структура, как это:

typedef struct { int x, y; } stPoint;

stPoint foo( void )
{
    stPoint point = { 1, 2 };
    return point;
}

int main( void )
{
    stPoint point = foo();
    printf( "%d %d\n", point.x, point.y );
}

это только один пример, где вы можете увидеть функцию, возвращающую структуру.