Чи безпечно повертати структуру на C або C ++?


84

Я розумію, що цього робити не слід, але я вважаю, що бачив приклади, що роблять щось подібне (код примітки не обов'язково синтаксично правильний, але ідея є)

typedef struct{
    int a,b;
}mystruct;

А далі ось функція

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

Я розумів, що нам завжди слід повертати вказівник на неправильно розроблену структуру, якщо ми хочемо зробити щось подібне, але я впевнений, що бачив приклади, які роблять щось подібне. Це правильно? Особисто я завжди або повертаю вказівник на неправильно розміщену структуру, або просто роблю передачу, посилаючись на функцію, і змінюю там значення. (Оскільки я розумію, що коли область дії функції закінчиться, будь-який стек, який був використаний для розподілу структури, можна замінити).

Додамо другу частину до запитання: чи це залежить від компілятора? Якщо це так, то якою є поведінка останніх версій компіляторів для робочих столів: gcc, g ++ та Visual Studio?

Думки з цього приводу?


33
"Я розумію, що цього робити не слід", - каже хто? Я роблю це весь час. Також зверніть увагу, що typedef не є необхідним у C ++, і що не існує такого поняття, як "C / C ++".
PlasmaHH

4
Здається, питання не орієнтоване на c ++.
Капітан Жираф

4
@PlasmaHH Копіювання великих структур навколо може бути неефективним. Ось чому слід бути обережним і добре подумати перед тим, як повертати структуру за значенням, особливо якщо у структурі є дорогий конструктор копіювання, а компілятор погано оптимізує повернене значення. Нещодавно я зробив оптимізацію програми, яка витрачала значну частину свого часу на конструктори копій для декількох великих структур, які один програміст вирішив повернути за цінністю скрізь. Неефективність коштувала нам близько 800 000 доларів додаткового обладнання для центру обробки даних, яке нам потрібно було придбати.
Crashworks

8
@Crashworks: Вітаю, сподіваюсь, ваш бос підняв вам підвищення.
PlasmaHH

6
@Crashworks: впевнений, що погано завжди повертати за значенням, не замислюючись, але в ситуаціях, коли це природно, зазвичай немає безпечної альтернативи, яка також не вимагає копіювання, тому повернення за значенням є найкращим рішенням, як це робиться не потребує розподілу купи. Часто навіть копії не буде , використовуючи хороший компілятор, слід скористатися копією, коли це можливо, і в C ++ 11 семантика переміщення може усунути ще більше глибокого копіювання. Обидва механізми не працюватимуть належним чином, якщо ви робите щось інше, крім повернення за значенням.
залишився біля

Відповіді:


77

Це абсолютно безпечно, і це не помилково. Також: він не залежить від компілятора.

Зазвичай, коли (як ваш приклад) ваша структура не надто велика, я б стверджував, що такий підхід навіть кращий, ніж повернення непрацюючої структури ( mallocце дорога операція).


3
Чи все-таки було б безпечно, якби одне з полів було символом *? Тепер у структурі був би покажчик
jzepeda

3
@ user963258 насправді, це залежить від того, як ви реалізуєте конструктор копіювання та деструктор.
Luchian Grigore

2
@PabloSantaCruz Це складне питання. Якби це було екзаменаційне запитання, екзаменатор цілком міг би очікувати "ні" як відповідь, якщо потрібно розглянути право власності.
Капітан Жираф

2
@CaptainGiraffe: правда. Оскільки OP не пояснив цього, і його / її приклади в основному були C , я припустив, що це більше питання C, ніж питання C ++ .
Пабло Санта-Крус

2
@Kos У деяких компіляторів немає NRVO? З якого року? Також зверніть увагу: у C ++ 11, навіть якщо він не має NRVO, він замість цього буде викликати семантику переміщення.
Девід

72

Це абсолютно безпечно.

Ви повертаєтесь за значенням. Що призвело б до невизначеної поведінки, якби ви поверталися за посиланням.

//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;
}

Поведінка вашого фрагмента є цілком допустимою та визначеною. Це не залежить від компілятора. Нічого страшного!

Особисто я завжди або повертаю вказівник на неправильно розміщену структуру

Ти не повинен. Слід уникати динамічно виділеної пам'яті, коли це можливо.

або просто зробіть передачу за посиланням на функцію та змініть там значення.

Цей варіант цілком дійсний. Це питання вибору. Загалом, ви робите це, якщо хочете повернути щось ще з функції, одночасно змінюючи вихідну структуру.

Оскільки я розумію, що коли область дії функції закінчиться, будь-який стек, який був використаний для розподілу структури, можна замінити

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


3
+1 за згадку про RVO. Ця важлива оптимізація насправді робить цей шаблон можливим для об’єктів із дорогими конструкторами копій, таких як контейнери STL.
Кос

1
Варто зазначити, що хоча компілятор може вільно виконувати оптимізацію поверненого значення, жодної гарантії цього не буде. Це не те, на що можна розраховувати, лише надія.
Watcom

-1 за "уникання динамічно виділеної пам'яті, коли це можливо". Це, як правило, правило newb, і часто призводить до появи коду, де повертається ВЕЛИКИЙ обсяг даних (і вони задають питання, чому все працює повільно), коли простий вказівник може заощадити багато часу. Правильним правилом є структури повернення або покажчики на основі швидкості , використання та ясність .
Ллойд Сарджент,

10

Час життя mystructоб’єкта у вашій функції дійсно закінчується, коли ви залишаєте функцію. Однак ви передаєте об'єкт за значенням у операторі return. Це означає, що об'єкт копіюється з функції у функцію, що викликає. Оригінальний об’єкт зник, але копія продовжує жити.


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

3
Я б сказав, що турбуєтеся про додаткову копію лише після того, як ви зробили трохи профілювання.
Luchian Grigore

4

Тип структури може бути типом значення, яке повертає функція. Це безпечно, оскільки компілятор збирається створити копію struct і повернути копію, а не локальну структуру у функції.

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.
Бьорн Сундін,

4

Повернути структуру, як ви це робили, абсолютно безпечно.

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


Стек використовується лише тимчасово для копіювання. Зазвичай стек резервується перед викликом, а викликана функція розміщує дані, що повертаються, у стек, а потім абонент витягує ці дані зі стеку і зберігає їх там, де йому призначено. Отже, ніяких турбот там.
Томас Темпельманн,

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

Ви можете знайти кілька повідомлень у списку розсилки вина про те, як, здається, це робить держава-член.


Було б корисніше, якби ви вказали вказівник на список розсилки вина, на який ви посилаєтесь.
Джонатан Леффлер,

Повернення структури - це нормально. COM визначає двійковий інтерфейс; якщо хтось не реалізує COM належним чином, це буде помилкою.
MM

1

Примітка: ця відповідь стосується лише c ++ 11 і далі. Не існує такого поняття, як "C / C ++", це різні мови.

Ні, не існує небезпеки повернення локального об’єкта за вартістю, і рекомендується це робити. Однак я думаю, що тут є важливий момент, який бракує у всіх відповідях тут. Багато інших говорили, що структура копіюється або розміщується безпосередньо за допомогою RVO. Однак це не зовсім правильно. Я спробую пояснити, що саме може статися при поверненні локального об'єкта.

Переміщення семантики

Починаючи з ++ ++, у нас є посилання 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 полягає у тому, щоб завжди віддавати перевагу поверненню об’єктів за значенням (якщо кілька повернутих значень, використовуйте struct (або кортеж)), де єдиним винятком є ​​випадки, коли об’єкт дорогий для переміщення. У цьому випадку використовуйте неконстантний посилальний параметр.

НІКОЛИ не є гарною ідеєю повертати ресурс, який потрібно вручну звільнити від функції в c ++. Ніколи цього не роби. Принаймні використовуйте std :: unique_ptr, або створіть власну нелокальну або локальну структуру за допомогою деструктора, який звільняє свій ресурс ( RAII ) і повертає екземпляр цього. Тоді також було б непоганою ідеєю визначити конструктор переміщення та оператор присвоєння переміщення, якщо ресурс не має власної семантики переміщення (та видалити конструктор копіювання / призначення).


цікаві факти. Я думаю, що в голангу є щось подібне.
Мокс
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.