Чому нестандартна посилання не може прив'язуватися до тимчасового об'єкта?


226

Чому заборонено отримувати неконтрастну посилання на тимчасовий об’єкт, який getx()повертається функцією ? Зрозуміло, що це заборонено стандартом C ++, але мене цікавить мета такого обмеження, а не посилання на стандарт.

struct X
{
    X& ref() { return *this; }
};

X getx() { return X();}

void g(X & x) {}    

int f()
{
    const X& x = getx(); // OK
    X& x = getx(); // error
    X& x = getx().ref(); // OK
    g(getx()); //error
    g(getx().ref()); //OK
    return 0;
}
  1. Зрозуміло, що термін експлуатації об'єкта не може бути причиною, оскільки постійне посилання на об'єкт не заборонено стандартом C ++.
  2. Зрозуміло, що тимчасовий об'єкт не є постійним у зразку, наведеному вище, тому що дозволені виклики до непостійних функцій дозволені. Наприклад, ref()може змінити тимчасовий об'єкт.
  3. Крім того, ref()дозволяє обдурити компілятор і отримати посилання на цей тимчасовий об’єкт, що вирішує нашу проблему.

В додаток:

Вони говорять, що "присвоєння тимчасового об'єкта посилання const подовжує термін експлуатації цього об'єкта" і "Хоча нічого не сказано про посилання, які не мають const". Моє додаткове запитання . Чи надає наступне призначення продовження терміну експлуатації тимчасового об'єкта?

X& x = getx().ref(); // OK

4
Я не погоджуюся з частиною "час експлуатації об'єкта не може бути причиною", лише тому, що в стандарті зазначено, що присвоєння тимчасового об'єкта посилання const подовжує термін експлуатації цього об'єкта до терміну дії посилання const. Про посилання, що не стосуються const, нічого не сказано ...
SadSido

3
Ну, в чому причина цього "Хоча нічого не сказано про неконституційні посилання ...". Це частина мого питання. Чи є в цьому сенс? Можливо, автори Standard просто забули про неконституційні посилання, і незабаром ми побачимо наступний Core Issue?
Олексій Малістов

2
GotW # 88: Кандидат на "Найважливіший конкурс". biljeutter.spaces.live.com/blog/cns!2D4327CC297151BB!378.entry
fnieto - Фернандо Нієто

4
@Michael: VC пов'язує rvalues ​​з не-const посиланнями. Вони називають цю особливість, але насправді це помилка. (Зауважте, що це не помилка, тому що вона сама по собі нелогічна, а тому, що виключено явно, щоб запобігти нерозумним помилкам.)
sbi

Відповіді:


97

З цієї статті блогу Visual C ++ про посилання на оцінку :

... C ++ не хоче, щоб ви випадково змінювали часові файли, але безпосередньо виклик функції, яка не містить const, на змінному rvalue є явним, тому це дозволено ...

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

Якби я був ти, я б переосмислив дизайн своїх функцій. Чому g () приймає посилання, він змінює параметр? Якщо ні, зробіть це const посиланням, якщо так, то чому ви намагаєтесь передати його тимчасово, чи вам не байдуже, це тимчасовий ви модифікуєте? Чому getx () повертається тимчасово? Якщо ви поділитеся з нами своїм реальним сценарієм і тим, що ви намагаєтеся здійснити, ви можете отримати кілька хороших пропозицій, як це зробити.

Іти проти мови та обдурити компілятор рідко вирішує проблеми - зазвичай це створює проблеми.


Редагувати: Адресуючи питання в коментарі: 1) X& x = getx().ref(); // OK when will x die?- Я не знаю і мені все одно, адже це саме те, що я маю на увазі під «протилежністю до мови». У мові сказано, що "тимчасові люди вмирають в кінці висловлювання, якщо тільки вони не зобов'язані конституційним посиланням; в цьому випадку вони гинуть, коли посилання виходить за межі". Застосовуючи це правило, здається, що x вже мертвий на початку наступного висловлювання, оскільки він не пов'язаний з const посиланням (компілятор не знає, що ref () повертає). Це лише здогадка.

2) Я чітко заявив про мету: вам не дозволяється змінювати часові теми, оскільки це просто не має сенсу (ігнорування посилань на C ++ 0x rvalue). Питання "тоді чому мені дозволяється дзвонити членам, які не беруть участь у конкурсі" це добре, але я не маю кращої відповіді, ніж той, про який я вже говорив вище.

3) Ну, якщо я маю рацію про те, що X& x = getx().ref();помер у кінці висловлювання, проблеми очевидні.

У будь-якому випадку, виходячи з вашого запитання та коментарів, я не думаю, що навіть ці додаткові відповіді вас не задовольнять. Ось остаточна спроба / резюме: Комітет C ++ вирішив, що не має сенсу змінювати часові сховища, тому вони забороняють прив'язувати до посилань, які не мають права. Можливо, якась реалізація компілятора чи історичні проблеми також були пов'язані, я не знаю. Потім з'явився якийсь конкретний випадок, і було вирішено, що проти всіх шансів вони все ж дозволять пряму модифікацію за допомогою виклику методу non-const. Але це виняток - вам, як правило, не дозволяється змінювати часописи. Так, C ++ часто буває дивним.


2
@sbk: 1) Власне, правильна фраза така: "... в кінці повного виразу ...". Я вважаю, що "повний вираз" визначається як той, що не є суб-виразом якогось іншого виразу. Чи завжди це збігається з "кінцем твердження", я не впевнений.
sbi

5
@sbk: 2) На насправді, ви які дозволяється змінювати rvalues (тимчасових). Це заборонено для вбудованих типів ( і intт.д.), але це допускається для визначених користувачем типів: (std::string("A")+"B").append("C").
sbi

6
@sbk: 3) Причина, яку Stroustrup дає (у D&E) забороняє прив'язувати rvalues ​​до non-const посилань, полягає в тому, що, якби Олексій змінив g()об'єкт (якого ви очікуєте від функції, яка має посилання non-const), це змінило б об'єкт, який загине, так що ніхто не міг отримати змінене значення в будь-якому випадку. Він каже, що це, швидше за все, помилка.
sbi

2
@sbk: Вибачте, якщо я образив вас, але я не вважаю, що 2) це нитко. Оцінки просто не const, якщо ви не зробите їх, і ви можете змінити їх, якщо вони не є вбудованими. Мені знадобилось певний час, щоб зрозуміти, чи, наприклад, з прикладом рядка, я щось не так роблю (JFTR: Не знаю), тому я схильний сприймати це розрізнення серйозно.
sbi

5
Я погоджуюсь з sbi - ця справа зовсім не принижена. Це вся основа семантики переміщення, що оцінювання типу класу найкраще зберігати без обмежень.
Йоханнес Шауб - ліб

38

У ваш код getx()повертається тимчасовий об’єкт, так зване "rvalue". Ви можете скопіювати rvalues ​​в об’єкти (ака. Змінні) або прив’язати їх до const посилань (що продовжить їхній термін служби до кінця життя посилання). Не можна прив'язувати rvalues ​​до посилань, які не мають const.

Це було цілеспрямоване дизайнерське рішення, щоб запобігти випадковому зміні об'єкта, який загине в кінці виразу:

g(getx()); // g() would modify an object without anyone being able to observe

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

X x1 = getx();
const X& x2 = getx(); // extend lifetime of temporary to lifetime of const reference

g(x1); // fine
g(x2); // can't bind a const reference to a non-const reference

Зауважте, що наступний C ++ стандарт буде містити посилання на оцінку. Отже, те, що ви знаєте як посилання, стає називатися "посилання на значення". Вам буде дозволено прив’язувати rvalues ​​до rvalue посилань, і ви можете перевантажувати функції на "rvalue-ness":

void g(X&);   // #1, takes an ordinary (lvalue) reference
void g(X&&);  // #2, takes an rvalue reference

X x; 
g(x);      // calls #1
g(getx()); // calls #2
g(X());    // calls #2, too

Ідея, що стоїть за посиланнями на рецензію, полягає в тому, що, оскільки ці об'єкти все одно загинуть, ви можете скористатися цим знанням і реалізувати те, що називається "переміщення семантики", певного виду оптимізації:

class X {
  X(X&& rhs)
    : pimpl( rhs.pimpl ) // steal rhs' data...
  {
    rhs.pimpl = NULL; // ...and leave it empty, but deconstructible
  }

  data* pimpl; // you would use a smart ptr, of course
};


X x(getx()); // x will steal the rvalue's data, leaving the temporary object empty

2
Привіт, це приголомшлива відповідь. Потрібно знати одне, але g(getx())це не працює, оскільки його підпис є g(X& x)і get(x)повертає тимчасовий об'єкт, тому ми не можемо прив’язати тимчасовий об’єкт ( rvalue ) до непостійної посилання, правильно? І у вашому першому const X& x2 = getx();const X& x1 = getx();
кодовому

1
Дякую, що вказали на цю помилку у своїй відповіді через 5 років після того, як я написав її! :-/Так, ваші міркування правильні, хоч і трохи зворотні: ми не можемо прив’язувати тимчасові посилання до не const(lvalue) посилань, і тому тимчасове повернення getx()(а не get(x)) не може бути пов'язане з посиланням на значення, яке є аргументом g().
sbi

Гм, що ти мав на увазі під getx()(а не get(x)) ?
SexyBeast

Коли я пишу "... getx () (а не get (x)) ..." , я маю на увазі, що ім'я функції є getx(), а не get(x)(як ви писали).
sbi

Ця відповідь змішує термінологію. Rvalue - категорія вираження. Тимчасовий об’єкт - це об’єкт. Рівне значення може або не може позначати тимчасовий об'єкт; і тимчасовий об'єкт може бути або не може бути позначений оцінкою.
ММ

16

Що ви показуєте, це те, що ланцюжок операторів дозволений.

 X& x = getx().ref(); // OK

Вираз "getx (). Ref ();" і це виконується до завершення перед призначенням "x".

Зауважте, що getx () не повертає посилання, а повністю сформований об'єкт у локальний контекст. Об'єкт тимчасовий, але він не const, що дозволяє викликати інші методи для обчислення значення або виникнення інших побічних ефектів.

// It would allow things like this.
getPipeline().procInstr(1).procInstr(2).procInstr(3);

// or more commonly
std::cout << getManiplator() << 5;

Подивіться в кінці цієї відповіді для кращого прикладу цього

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

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

x& = const_cast<x&>(getX());

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

Отже, єдине питання, що залишається, чому стандарт не хоче дозволити тимчасовим посиланням продовжити термін експлуатації об'єкта за межі кінця твердження?

Я вважаю, це тому, що це зробить компілятор дуже важко виправити тимчасові об'єкти. Це було зроблено для const посилань на тимчасові, оскільки це обмежене використання, і, таким чином, змусило вас зробити копію об'єкта, щоб зробити що-небудь корисне, але забезпечує деяку обмежену функціональність.

Подумайте про цю ситуацію:

int getI() { return 5;}
int x& = getI();

x++; // Note x is an alias to a variable. What variable are you updating.

Подовження терміну експлуатації цього тимчасового об'єкта буде дуже заплутаним.
Хоча наступне:

int const& y = getI();

Дасть вам код, який інтуїтивно використовувати та розуміти.

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


3
"Отже, єдине питання, що залишається, це те, чому стандарт не хоче дозволити посиланням на тимчасові компанії продовжити термін служби об'єкта за межі кінця заяви?" Це все! Ви розумієте моє запитання. Але я не згоден з вашою думкою. Ви кажете "зробити компілятор дуже важким", але це було зроблено для довідки const. Ви говорите у своєму зразку "Примітка x - псевдонім змінної. Яку змінну ви оновлюєте." Нема проблем. Є унікальна змінна (тимчасова). Деякі тимчасові об’єкти (дорівнює 5) необхідно змінити.
Олексій Малістов

@Martin: Посилання на данглінг не лише неохайні. Вони можуть призвести до серйозних помилок при зверненні пізніше до методу!
мммммммммммм

1
@ Алекс: Зауважимо, що факт прив'язки його до посилання const збільшує час життя тимчасового періоду - виняток, який додається навмисно (TTBOMK, щоб дозволити ручну оптимізацію). Не було додано винятків для не-const посилань, тому що прив'язка тимчасової до non-const посилань вважалася, швидше за все, помилкою програміста.
sbi

1
@alexy: Посилання на невидиму змінну! Не така інтуїтивна.
Мартін Йорк

const_cast<x&>(getX());не має сенсу
допитливо

10

Чому це обговорюється в FAQ + C ( сміливе шахта):

У C ++ посилання, які не мають const, можуть пов'язуватися з lvalues, а посилання const можуть пов'язуватися з lvalues ​​або rvalues, але немає нічого, що може прив'язуватися до rvalue, що не має const. Це для захисту людей від зміни цінностей тимчасових знищень, перш ніж їх можна використовувати нову цінність . Наприклад:

void incr(int& a) { ++a; }
int i = 0;
incr(i);    // i becomes 1
incr(0);    // error: 0 is not an lvalue

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


Було б смішно бачити обличчя програміста, якого покусав той фортранський «нульовий помилок»! x * 0 дає x? Що? Що??
Джон Д

Цей останній аргумент особливо слабкий. Жоден компілятор, який варто згадати, ніколи не змінив би значення 0 на 1 або навіть інтерпретував incr(0);таким чином. Очевидно, якби це було дозволено, це було б інтерпретовано як створення тимчасового цілого числа та передача його доincr()
user3204459

6

Головне питання в тому

g(getx()); //error

- це логічна помилка: gце зміна результату, getx()але ви не маєте шансів вивчити модифікований об'єкт. Якщо gне потрібно було змінювати його параметр, тоді він не потребував би посилання lvalue, він міг би взяти параметр за значенням або за посиланням const.

const X& x = getx(); // OK

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

Однак зробити це неможливо

X& x = getx(); // error

Дійсно, не роблячи g(getx())дійсних, чого саме мовні дизайнери намагалися уникнути в першу чергу.

g(getx().ref()); //OK

є дійсним, тому що методи знають лише про thisстабільність, вони не знають, чи викликаються вони на lvalue або rvalue.

Як завжди в C ++, у вас є рішення цього правила, але ви повинні повідомити компілятору, що ви знаєте, що ви робите, чітко:

g(const_cast<x&>(getX()));

5

Схоже, на початкове запитання про те, чому це заборонено, було чітко відповідено: "тому що це, швидше за все, помилка".

FWIW, я думав, що я покажу, як це можна зробити, хоча я не думаю, що це хороша техніка.

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

// Assuming: void Person::GetNameAndAddr(std::string &name, std::string &addr);
string name;
person.GetNameAndAddr(name, string()); // don't care about addr

Як пояснено в попередніх відповідях, це не складається. Але це компілюється і працює правильно (з моїм компілятором):

person.GetNameAndAddr(name,
    const_cast<string &>(static_cast<const string &>(string())));

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

string name;
string unused;
person.GetNameAndAddr(name, unused); // don't care about addr

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

string name;
{
    string unused;
    person.GetNameAndAddr(name, unused); // don't care about addr
}

- Кріс


4

Чому б ти хотів колись X& x = getx();? Просто використовуйте X x = getx();і покладайтеся на RVO.


3
Тому що я хочу зателефонувати, g(getx())а неg(getx().ref())
Олексій Малістов

4
@ Алекс, це не реальна причина. Якщо ви це зробите, то у вас виникає логічна помилка, тому що виправите gщось, до чого більше не можете дістатись.
Йоханнес Шауб - ліб

3
@ JohannesSchaub-litb, можливо, йому все одно.
допитливий хлопець

" покладатися на RVO ", за винятком того, що це не називається "RVO".
curiousguy

2
@curiousguy: Це дуже прийнятий термін для цього. Немає нічого поганого в тому, щоб називати це "RVO".
Щеня

4

Злий спосіб вирішення стосується ключового слова "змінний". Власне бути злом залишається як вправа для читача. Або дивіться тут: http://www.ddj.com/cpp/184403758


3

Відмінне запитання, і ось моя спроба отримати більш коротку відповідь (оскільки багато корисної інформації є в коментарях і важко розкопатися в шумі).

Будь-яке посилання, пов'язане безпосередньо з тимчасовим, продовжить його життя [12.2.5]. З іншого боку, посилання, ініціалізоване з іншою посиланням, не буде (навіть якщо воно в кінцевому підсумку є тимчасовим). Це має сенс (компілятор не знає, на що в кінцевому підсумку посилається).

Але вся ця ідея надзвичайно заплутана. Наприклад const X &x = X();, тимчасові триватимуть, поки xпосилання, але const X &x = X().ref();НЕ буде (хто знає, що ref()насправді повернулося). В останньому випадку деструктор дляX виклику стає в кінці цього рядка. (Це можна спостерігати за нетривіальним деструктором.)

Тож це здається загалом заплутаним та небезпечним (чому ускладнювати правила щодо життя об'єкта?), Але, мабуть, була потреба хоча б у довідках про const, тому стандарт встановлює цю поведінку для них.

[З коментаря sbi ]: Зауважте, що факт прив’язки його до const-посилання збільшує час життя тимчасового періоду - виняток, який було додано навмисно (TTBOMK, щоб дозволити ручну оптимізацію). Не було додано винятків для не-const посилань, тому що прив'язка тимчасової до non-const посилань вважалася, швидше за все, помилкою програміста.

Усі тимчасовості зберігаються до кінця висловлювання. Однак, щоб скористатися ними, вам потрібен трюк, як у вас ref(). Це законно. Мабуть, немає вагомих причин для того, щоб зайвий обруч перескочив, окрім нагадування програмісту про те, що відбувається щось незвичне (а саме - еталонний параметр, модифікації якого будуть швидко втрачені).

[Інший коментар sbi ] Причина, яку Stroustrup дає (у D&E) забороняє прив'язувати rvalues ​​до non-const посилань, полягає в тому, що якщо Alexey's g () змінив би об'єкт (чого можна очікувати від функції, яка приймає non-const посилання), це змінило б об'єкт, який загине, тому ніхто не міг отримати змінене значення в будь-якому випадку. Він каже, що це, швидше за все, помилка.


2

"Зрозуміло, що тимчасовий об'єкт не є постійним у наведеному вище зразку, тому що виклики нестаціонарних функцій дозволені. Наприклад, ref () може змінити тимчасовий об'єкт."

У вашому прикладі getX () не повертає const X, тому ви можете викликати ref () приблизно так само, як ви могли викликати X (). Ref (). Ви повертаєте non-const ref і тому можете зателефонувати не const-методам, те, що ви не можете зробити, це призначити посилання non-const.

Поряд із коментарем SadSidos, це робить ваші три моменти неправильними.


1

У мене є сценарій, про який я хотів би поділитися там, де я хотів би зробити те, що просить Олексій. У плагіні May C ++ я повинен зробити наступний shenanigan, щоб отримати значення в атрибуті вузла:

MFnDoubleArrayData myArrayData;
MObject myArrayObj = myArrayData.create(myArray);   
MPlug myPlug = myNode.findPlug(attributeName);
myPlug.setValue(myArrayObj);

Це втомливо писати, тому я написав такі допоміжні функції:

MPlug operator | (MFnDependencyNode& node, MObject& attribute){
    MStatus status;
    MPlug returnValue = node.findPlug(attribute, &status);
    return returnValue;
}

void operator << (MPlug& plug, MDoubleArray& doubleArray){
    MStatus status;
    MFnDoubleArrayData doubleArrayData;
    MObject doubleArrayObject = doubleArrayData.create(doubleArray, &status);
    status = plug.setValue(doubleArrayObject);
}

І тепер я можу написати код з початку публікації як:

(myNode | attributeName) << myArray;

Проблема полягає в тому, що вона не компілюється поза Visual C ++, оскільки вона намагається прив’язати тимчасову змінну, повернуту з | оператора на посилання MPlug оператора <<. Мені б хотілося, щоб це було посиланням, оскільки цей код називається багато разів, і я вважаю за краще не копіювати MPlug так сильно. Мені потрібен лише тимчасовий об’єкт, щоб жити до кінця другої функції.

Ну, це мій сценарій. Я просто подумав, що я покажу приклад, де хотілося б зробити те, що описав Олексій. Я вітаю всі критики та пропозиції!

Дякую.

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