Безопасно ли возвращать структуру на C или C ++?

85

Я понимаю, что этого делать не следует, но я считаю, что видел примеры, которые делают что-то подобное (код заметки не обязательно синтаксически правильный, но идея есть)

typedef struct{
    int a,b;
}mystruct;

А вот функция

mystruct func(int c, int d){
    mystruct retval;
    retval.a = c;
    retval.b = d;
    return retval;
}

Я понял, что мы всегда должны возвращать указатель на malloc'ed struct, если мы хотим сделать что-то подобное, но я уверен, что видел примеры, которые делают что-то подобное. Это верно? Лично я всегда либо возвращаю указатель на malloc'ed структуру, либо просто передаю ссылку на функцию и изменяю там значения. (Поскольку я понимаю, что как только область действия функции завершена, любой стек, который использовался для выделения структуры, может быть перезаписан).

Добавим вторую часть к вопросу: это зависит от компилятора? Если да, то каково поведение последних версий компиляторов для настольных компьютеров: gcc, g ++ и Visual Studio?

Мысли по этому поводу?

Jzepeda
источник
34
«Я понимаю, что этого делать нельзя», - говорит кто? Я делаю это все время. Также обратите внимание, что typedef не требуется в C ++ и что не существует такой вещи, как «C / C ++».
PlasmaHH 06
4
Вопрос, похоже, не нацелен на С ++.
Captain Giraffe
4
@PlasmaHH Копирование больших структур может быть неэффективным. Вот почему нужно быть осторожным и хорошо подумать, прежде чем возвращать структуру по значению, особенно если у структуры есть дорогой конструктор копирования, а компилятор не умеет оптимизировать возвращаемое значение. Недавно я оптимизировал приложение, которое тратило значительную часть своего времени на конструкторы копирования для нескольких больших структур, которые один программист решил везде возвращать по значению. Неэффективность обошлась нам примерно в 800 000 долларов в приобретении дополнительного оборудования для центров обработки данных.
Crashworks 06
8
@Crashworks: Поздравляю, надеюсь, ваш босс повысил вам зарплату.
PlasmaHH 06
6
@Crashworks: конечно, плохо всегда возвращаться по значению, не задумываясь, но в ситуациях, когда это естественно, обычно нет безопасной альтернативы, которая также не требует создания копии, поэтому возвращение по значению - лучшее решение, поскольку оно есть не требует выделения кучи. Часто даже копии не будет , использование хорошего компилятора copy elision должно включиться , когда это возможно, а в C ++ 11 семантика перемещения может устранить еще больше глубокого копирования. Оба механизма не будут работать должным образом, если вы сделаете что - нибудь еще, кроме возврата по значению.
leftaround примерно 06

Ответы:

78

Это совершенно безопасно, и в этом нет ничего плохого. Также: это не зависит от компилятора.

Обычно, когда (например, ваш пример) ваша структура не слишком велика, я бы сказал, что этот подход даже лучше, чем возврат malloc'ed структуры ( mallocэто дорогостоящая операция).

Пабло Санта Крус
источник
3
Было бы безопасно, если бы одно из полей было char *? Теперь в структуре будет указатель
jzepeda 06
3
На самом деле @ user963258, это зависит от того, как вы реализуете конструктор и деструктор копирования.
Лучиан Григоре
2
@PabloSantaCruz Это сложный вопрос. Если это экзаменационный вопрос, экзаменатор вполне может ожидать ответа «нет», если необходимо рассмотреть вопрос о праве собственности.
Captain Giraffe
2
@CaptainGiraffe: правда. Поскольку OP не разъяснил это, а его / ее примеры были в основном на C , я предположил, что это был скорее вопрос C, чем вопрос C ++ .
Пабло Санта-Крус,
2
@Kos У некоторых компиляторов нет NRVO? С какого года? Также следует отметить: в C ++ 11, даже если он не имеет NRVO, вместо этого будет использоваться семантика перемещения.
Дэвид
73

Это совершенно безопасно.

Вы возвращаетесь по стоимости. Что привело бы к неопределенному поведению, если бы вы возвращались по ссылке.

//safe
mystruct func(int c, int d){
    mystruct retval;
    retval.a = c;
    retval.b = d;
    return retval;
}

//undefined behavior
mystruct& func(int c, int d){
    mystruct retval;
    retval.a = c;
    retval.b = d;
    return retval;
}

Поведение вашего сниппета совершенно корректно и определено. Это не зависит от компилятора. Все нормально!

Лично я всегда либо возвращаю указатель на malloc'ed struct

Вы не должны. По возможности следует избегать динамически выделяемой памяти.

или просто выполните переход по ссылке на функцию и измените там значения.

Этот вариант вполне допустим. Это вопрос выбора. В общем, вы делаете это, если хотите вернуть что-то еще из функции, изменяя исходную структуру.

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

Это не правильно. Я имел в виду, что это вроде как правильно, но вы возвращаете копию структуры, которую создаете внутри функции. Теоретически . На практике RVO может и, вероятно, произойдет. Прочтите об оптимизации возвращаемого значения. Это означает, что хотя retvalкажется, что после завершения функции она выходит за пределы области видимости, на самом деле она может быть построена в контексте вызова, чтобы предотвратить дополнительную копию. Это оптимизация, которую компилятор может реализовать бесплатно.

Лучиан Григоре
источник
3
+1 за упоминание РВО. Эта важная оптимизация фактически делает этот шаблон применимым для объектов с дорогостоящими конструкторами копирования, такими как контейнеры STL.
Кос,
1
Стоит отметить, что, хотя компилятор может выполнять оптимизацию возвращаемого значения, нет никакой гарантии, что это будет. Это не то, на что можно рассчитывать, только надежда.
Watcom
-1 за «избегание динамически выделяемой памяти, когда это возможно». Это, как правило, новое правило и часто приводит к тому, что код возвращает БОЛЬШИЕ объемы данных (и они ломают голову, почему все работает медленно), когда простой указатель может сэкономить много времени. Правильное правило - возвращать структуры или указатели на основе скорости , использование и ясность .
Ллойд Сарджент,
10

Время жизни mystructобъекта в вашей функции действительно заканчивается, когда вы покидаете функцию. Однако вы передаете объект по значению в операторе возврата. Это означает, что объект копируется из функции в вызывающую функцию. Исходный объект исчез, но копия продолжает жить.

Джозеф Мэнсфилд
источник
9

Не только безопасно возвращать a structв C (или a classв C ++, где struct-s фактически являются class-es с public:членами по умолчанию ), но и многие программы делают это.

Конечно, при возврате a classв C ++ язык указывает, что будет вызываться некоторый деструктор или движущийся конструктор, но есть много случаев, когда это может быть оптимизировано компилятором.

Кроме того, Linux x86-64 ABI указывает, что возврат a structс двумя скалярными (например, указателями или long) значениями осуществляется через регистры ( %rax& %rdx), поэтому это очень быстро и эффективно. Так что в этом конкретном случае, вероятно, быстрее вернуть такие двухскалярные поля, structчем делать что-либо еще (например, сохранять их в указателе, переданном в качестве аргумента).

Таким образом, возврат такого двухскалярного поля structпроисходит намного быстрее, чем malloc-ing его и возврат указателя.

Василий Старынкевич
источник
5

Это совершенно законно, но при работе с большими структурами необходимо учитывать два фактора: скорость и размер стека.

ебутусов
источник
1
Слышали об оптимизации возврата?
Лучиан Григоре
Да, но мы говорим в общем о возврате структуры по значению, и бывают случаи, когда компилятор не может выполнить RVO.
ebutusov 06
3
Я бы сказал, что беспокойтесь о лишней копии только после того, как вы выполните профилирование.
Лучиан Григоре
4

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

typedef struct{
    int a,b;
}mystruct;

mystruct func(int c, int d){
    mystruct retval;
    cout << "func:" <<&retval<< endl;
    retval.a = c;
    retval.b = d;
    return retval;
}

int main()
{
    cout << "main:" <<&(func(1,2))<< endl;


    system("pause");
}
хабердар
источник
4

Безопасность зависит от того, как была реализована сама структура. Я просто наткнулся на этот вопрос, когда реализовал нечто подобное, и вот потенциальная проблема.

Компилятор при возврате значения выполняет несколько операций (среди возможных других):

  1. Вызывает конструктор копирования mystruct(const mystruct&)( thisэто временная переменная вне функции, funcвыделенной самим компилятором)
  2. вызывает деструктор ~mystructпеременной, которая была размещена внутриfunc
  3. вызывает, mystruct::operator=если возвращаемое значение присвоено чему-то еще с=
  4. вызывает деструктор ~mystructвременной переменной, используемой компилятором

Теперь, если mystructэто так просто, как описано здесь, все в порядке, но если у него есть указатель (например char*) или более сложное управление памятью, тогда все зависит от того mystruct::operator=, как mystruct(const mystruct&), и ~mystructреализованы. Поэтому я предлагаю соблюдать осторожность при возврате сложных структур данных в качестве значения.

Филиппо
источник
Верно только до c ++ 11.
Björn Sundin
4

Совершенно безопасно вернуть структуру, как вы это сделали.

Однако, основываясь на этом утверждении: поскольку я понимаю, что после того, как область действия функции завершена, любой стек, использованный для выделения структуры, может быть перезаписан, я могу представить только сценарий, в котором любой из членов структуры был динамически выделен ( malloc'ed или new'ed), и в этом случае без RVO динамически выделяемые члены будут уничтожены, а возвращенная копия будет иметь член, указывающий на мусор.

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

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

Ниже приведен простой пример подтверждения (взято из сборки компилятора Mingw).

_func:
    push    ebp
    mov ebp, esp
    sub esp, 16
    mov eax, DWORD PTR [ebp+8]
    mov DWORD PTR [ebp-8], eax
    mov eax, DWORD PTR [ebp+12]
    mov DWORD PTR [ebp-4], eax
    mov eax, DWORD PTR [ebp-8]
    mov edx, DWORD PTR [ebp-4]
    leave
    ret

Вы можете видеть, что значение b было передано через edx. в то время как eax по умолчанию содержит значение для.

опасный мозг
источник
2

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

У меня был более развернутый ответ, но модератору он не понравился. Итак, за ваш счет, мой совет короткий.

Игорь Полк
источник
«Возвращать строение небезопасно. […] Будет вызван конструктор копирования ». - Есть разница между безопасным и неэффективным . Возврат структуры определенно безопасен. Даже в этом случае вызов копирующего ctor, скорее всего, будет отклонен компилятором, поскольку структура для начала создается в стеке вызывающего.
phg
2

Добавим вторую часть к вопросу: это зависит от компилятора?

Действительно, как я обнаружил, к своей боли: http://sourceforge.net/p/mingw-w64/mailman/message/33176880/

Я использовал gcc на win32 (MinGW) для вызова COM-интерфейсов, возвращающих структуры. Оказывается, MS делает это иначе, чем GNU, и поэтому моя программа (gcc) вылетела из-за разбитого стека.

Может быть, здесь MS может иметь более высокое положение, но все, что меня волнует, - это совместимость ABI между MS и GNU для построения в Windows.

Если да, то каково поведение последних версий компиляторов для настольных ПК: gcc, g ++ и Visual Studio

В списке рассылки Wine вы можете найти несколько сообщений о том, как MS это делает.

effbiae
источник
Было бы более полезно, если бы вы указали указатель на список рассылки Wine, на который вы ссылаетесь.
Джонатан Леффлер
Возврат структур в порядке. COM определяет двоичный интерфейс; если кто-то не реализует COM должным образом, это будет ошибкой.
MM
1

Примечание: этот ответ относится только к C ++ 11 и далее. Нет такого понятия, как "C / C ++", это разные языки.

Нет, возвращать локальный объект по значению не опасно, и это рекомендуется делать. Однако я думаю, что во всех ответах здесь отсутствует важный момент. Многие другие сказали, что структура либо копируется, либо размещается напрямую с помощью RVO. Однако это не совсем так. Я постараюсь объяснить, что именно может произойти при возврате локального объекта.

Семантика перемещения

Начиная с C ++ 11, у нас были ссылки на rvalue, которые являются ссылками на временные объекты, которые можно безопасно украсть. Например, std :: vector имеет конструктор перемещения, а также оператор присваивания перемещения. Оба они имеют постоянную сложность и просто копируют указатель на данные перемещаемого вектора. Я не буду здесь вдаваться в подробности семантики перемещения.

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

Обратите внимание, что в вашем конкретном примере нет разницы между копированием и перемещением объекта. Конструкторы перемещения и копирования по умолчанию для вашей структуры приводят к одним и тем же операциям; копирование двух целых чисел. Однако это по крайней мере так же быстро, как любое другое решение, потому что вся структура помещается в 64-битный регистр ЦП (поправьте меня, если я ошибаюсь, я не знаю много регистров ЦП).

RVO и NRVO

RVO означает оптимизацию возвращаемого значения и является одной из немногих оптимизаций, которые делают компиляторы и которые могут иметь побочные эффекты. Начиная с c ++ 17 требуется RVO. При возврате безымянного объекта он создается непосредственно на месте, где вызывающий объект присваивает возвращаемое значение. Ни конструктор копирования, ни конструктор перемещения не вызываются. Без RVO безымянный объект сначала был бы построен локально, затем был бы создан перемещением по возвращенному адресу, а затем локальный безымянный объект был бы разрушен.

Пример, когда требуется RVO (c ++ 17) или вероятно (до c ++ 17):

auto function(int a, int b) -> MyStruct {
    // ...
    return MyStruct{a, b};
}

NRVO означает оптимизацию именованного возвращаемого значения и то же самое, что и RVO, за исключением того, что это делается для именованного объекта, локального для вызываемой функции. Это все еще не гарантируется стандартом (c ++ 20), но многие компиляторы все еще это делают. Обратите внимание, что даже с именованными локальными объектами они в худшем случае перемещаются при возврате.

Заключение

Единственный случай, когда вам следует рассмотреть возможность отказа от возврата по значению, - это когда у вас есть именованный, очень большой (как по размеру стека) объект. Это связано с тем, что NRVO еще не гарантирован (начиная с C ++ 20), и даже перемещение объекта будет медленным. Моя рекомендация и рекомендация в Cpp Core Guidelines - всегда отдавать предпочтение возврату объектов по значению (если возвращаются несколько значений, используйте структуру (или кортеж)), где единственное исключение - когда перемещение объекта дорого. В этом случае используйте неконстантный ссылочный параметр.

НИКОГДА не рекомендуется возвращать ресурс, который нужно вручную освободить из функции в C ++. Никогда не делай этого. По крайней мере, используйте std :: unique_ptr или создайте свою собственную нелокальную или локальную структуру с деструктором, который освобождает свой ресурс ( RAII ) и возвращает экземпляр этого. Тогда также было бы неплохо определить конструктор перемещения и оператор присваивания перемещения, если ресурс не имеет собственной семантики перемещения (и удалить конструктор / присваивание копии).

Бьорн Сундин
источник
Интересные факты. Я думаю, что у голанга есть нечто подобное.
Mox