Хто винен у цьому діапазоні на основі надсилання посилань на тимчасові?


15

Наступний код на перший погляд виглядає досить нешкідливим. Користувач використовує функцію bar()для взаємодії з деякими функціями бібліотеки. (Це, можливо, навіть давно працювало, оскільки bar()повернуло посилання на тимчасове значення чи подібне.) Однак це просто повернення нового екземпляра B. Bзнову має функцію, a()яка повертає посилання на об'єкт ітеративного типу A. Користувач хоче запитувати цей об'єкт, що призводить до сегмента за замовчуванням, оскільки тимчасовий Bоб'єкт, повернутий тим bar(), знищується до початку ітерації.

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

(Пов'язане запитання може бути: чи слід встановити загальне правило, що код не повинен "базуватися на ітерації" для чогось, що отримується більш ніж одним ланцюговим викликом у заголовку циклу, оскільки будь-який з цих викликів може повернути a оцінювати?)

#include <algorithm>
#include <iostream>

// "Library code"
struct A
{
    A():
        v{0,1,2}
    {
        std::cout << "A()" << std::endl;
    }

    ~A()
    {
        std::cout << "~A()" << std::endl;
    }

    int * begin()
    {
        return &v[0];
    }

    int * end()
    {
        return &v[3];
    }

    int v[3];
};

struct B
{
    A m_a;

    A & a()
    {
        return m_a;
    }
};

B bar()
{
    return B();
}

// User code
int main()
{
    for( auto i : bar().a() )
    {
        std::cout << i << std::endl;
    }
}

6
Коли ви зрозуміли, хто винен, що буде наступним кроком? Кричати на нього / її?
JensG

7
Ні, навіщо мені? Мені насправді цікавіше знати, де продуманий процес розробки цієї "програми" не зміг уникнути цієї проблеми в майбутньому.
hllnll

Це не має нічого спільного з rvalues ​​або на основі діапазону для циклів, але користувач не правильно розуміє термін експлуатації об'єкта.
Джеймс

Зауваження на сайті: Це CWG 900, який було закрито як "Не дефект". Можливо, протоколи містять певну дискусію.
деп

8
Хто винен у цьому? Б'ярн Струструп та Денніс Річі, насамперед.
Мейсон Уілер

Відповіді:


14

Я думаю, що основна проблема полягає в поєднанні мовних особливостей (або їх відсутності) C ++. І код бібліотеки, і код клієнта є розумними (про що свідчить той факт, що проблема далеко не очевидна). Якби термін служби тимчасового Bбув придатний подовжений (до кінця циклу), не було б проблем.

Зробити життя тимчасово досить довго, і вже не, надзвичайно важко. Навіть не так спеціально "всі тимчасові організації, які беруть участь у створенні діапазону для діапазону для прямої трансляції до кінця циклу", не мали б побічних ефектів. Розглянемо випадок B::a()повернення діапазону, незалежного від Bоб'єкта за значенням. Тоді тимчасове Bможна негайно відкинути. Навіть якби можна було точно визначити випадки, коли необхідне продовження терміну служби, оскільки ці випадки програмістам не очевидні, ефект (деструктори назвали набагато пізніше) був би дивовижним і, можливо, не менш тонким джерелом помилок.

Більш бажано було б просто виявити та заборонити такі дурниці, змусивши програміста явно піднятись bar()до локальної змінної. Це неможливо в C ++ 11, і, ймовірно, ніколи не стане можливим, оскільки для цього потрібні анотації. Іржа робить це, коли підпис .a()буде:

fn a<'x>(bar: &'x B) -> &'x A { bar.a }
// If we make it as explicit as possible, or
fn a(&self) -> &A { self.a }
// if we make it a method and rely on lifetime elision.

Ось 'xзмінна чи регіональна область життя, яка є символічною назвою за період часу, коли доступний ресурс. Відверто кажучи, життя важко пояснити - або ми ще не з’ясували найкращого пояснення - тому я обмежуся мінімальним необхідним для цього прикладом і віднесу схильного читача до офіційної документації .

Контролер позики зауважив, що результат bar().a()повинен жити до тих пір, поки цикл працює. Висловлюючись як обмеження на час життя 'x, ми пишемо: 'loop <= 'x. Також було б помічено, що приймач виклику методу bar(), є тимчасовим. Два покажчики пов'язані з однаковим життям, отже, 'x <= 'tempє ще одним обмеженням.

Ці два обмеження суперечливі! Нам потрібно , 'loop <= 'x <= 'tempале 'temp <= 'loop, яка захоплює цю проблему досить точно. Через суперечливі вимоги баггі-код відхиляється. Зауважте, що це перевірка часу компіляції, а код Rust зазвичай призводить до того ж машинного коду, що і еквівалентний код C ++, тому вам не потрібно платити за нього час виконання.

Тим не менш, це велика функція, яку можна додати до мови, і працює лише в тому випадку, якщо в ній використовується весь код. Дизайн API також впливає (деякі проекти, які були б занадто небезпечними в C ++, стають практичними, інші не можуть змусити себе грати добре з життям). На жаль, це означає, що додавати в C ++ (або будь-яку мову справді) не можна заднім числом. Підсумовуючи, виною є інерційність успішних мов, а також той факт, що Бьярне в 1983 році не мав кришталевого кулі та передбачення, щоб включити уроки останніх 30 років досліджень та досвід C ++ ;-)

Звичайно, це зовсім не допомагає уникнути проблеми в майбутньому (якщо тільки ви не перейдете на Rust і більше ніколи не використовуєте C ++). Можна уникнути більш тривалих виразів з декількома ланцюговими викликами методів (що досить обмежує і навіть не віддалено виправляє всі проблеми з життя). Або можна спробувати прийняти більш дисципліновану політику власності без допомоги компілятора: Документ чітко підтверджує те, що barповертається за значенням, і що результат B::a()не повинен переживати те, Bна що a()викликається. Змінюючи функцію на повернення за значенням замість довготривалої посилання, майте на увазі, що це зміна договору . Все-таки схильна до помилок, але може пришвидшити процес виявлення причини, коли вона все-таки відбудеться.


14

Чи можна вирішити цю проблему за допомогою функцій C ++?

C ++ 11 додав перекваліфікатори функції члена, що дозволяє обмежити категорію значень екземпляра класу (виразу), на який може бути викликана функція-член. Наприклад:

struct foo {
    void bar() & {} // lvalue-ref-qualified
};

foo& lvalue ();
foo  prvalue();

lvalue ().bar(); // OK
prvalue().bar(); // error

Під час виклику функції beginчлена ми знаємо, що, швидше за все, нам також потрібно буде викликати функцію endчлена (або щось подібнеsize , щоб отримати розмір діапазону). Це вимагає, щоб ми працювали над lvalue, оскільки нам потрібно вирішити це двічі. Ви можете, таким чином, стверджувати, що ці функції членів повинні бути кваліфіковані за значенням.

Однак це може не вирішити основну проблему: псевдонім. Функція beginі endчлен псевдоніму об'єкта або ресурсів, якими управляє об'єкт. Якщо ми замінимо beginі endоднією функцією range, ми повинні надати ту, яку можна викликати у rvalues:

struct foo {
    vector<int> arr;

    auto range() & // C++14 return type deduction for brevity
    { return std::make_pair(arr.begin(), arr.end()); }
};

for(auto const& e : foo().range()) // error

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

struct foo {
    vector<int> arr;

    auto range() &
    { return std::make_pair(arr.begin(), arr.end()); }

    auto range() &&
    { return std::move(arr); }
};

for(auto const& e : foo().range()) // OK

Застосування цього до справи ОП та невеликий перегляд коду

struct B {
    A m_a;
    A & a() { return m_a; }
};

Ця функція-член змінює категорію значення виразу: B()є первинним значенням, але B().a()є значенням. З іншого боку, B().m_aце ревальвер. Тож почнемо з того, щоб зробити це послідовним. Це можна зробити двома способами:

struct B {
    A m_a;
    A &  a() &  { return m_a; }

    A && a() && { return std::move(m_a); }
    // or
    A    a() && { return std::move(m_a); }
};

Як сказано вище, друга версія виправить це питання в ОП.

Крім того, ми можемо обмежити Bфункції члена:

struct A {
    // [...]

    int * begin() & { return &v[0]; }
    int * end  () & { return &v[3]; }

    int v[3];
};

Це не вплине на код OP, оскільки результат вираження після :циклу, заснованого на діапазоні, пов'язаний з посилальною змінною. І ця змінна (як вираз, що використовується для доступу до її функцій beginта endчленів) є значенням.

Зрозуміло, питання полягає в тому, чи має чи не за замовчуванням правило "вилучення функцій члена на rvalues ​​має повертати об'єкт, який володіє всіма його ресурсами, якщо тільки немає вагомих причин цього не робити" . Псевдонім, який він повертає, може легально використовувати, але він небезпечний тим, як ви його відчуваєте: його не можна використовувати для продовження терміну служби "батьківського" тимчасового:

// using the OP's definition of `struct B`,
// or version 1, `A && a() &&;`

A&&      a = B().a(); // bug: binds directly, dangling reference
A const& a = B().a(); // bug: same as above
A        a = B().a(); // OK

A&&      a = B().m_a; // OK: extends the lifetime of the temporary

У C ++ 2a, я думаю, ви повинні вирішити цю проблему (або подібну проблему) наступним чином:

for( B b = bar(); auto i : b.a() )

замість ОП

for( auto i : bar().a() )

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

Пропозиція, яка представила цю init-заяву

Демонстраційна демонстрація


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