Це відомий підводний камінь C ++ 11 для циклів?


89

Уявімо, що у нас є структура для проведення 3 дублів з деякими функціями-членами:

struct Vector {
  double x, y, z;
  // ...
  Vector &negate() {
    x = -x; y = -y; z = -z;
    return *this;
  }
  Vector &normalize() {
     double s = 1./sqrt(x*x+y*y+z*z);
     x *= s; y *= s; z *= s;
     return *this;
  }
  // ...
};

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

Vector v = ...;
v.normalize().negate();

Або навіть:

Vector v = Vector{1., 2., 3.}.normalize().negate();

Тепер, якщо ми надали функції begin () і end (), ми могли б використовувати наш Vector у новому стилі for loop, скажімо, цикл над 3 координатами x, y та z (можна, без сумніву, побудувати більше "корисних" прикладів шляхом заміни Vector на, наприклад, String):

Vector v = ...;
for (double x : v) { ... }

Ми навіть можемо зробити:

Vector v = ...;
for (double x : v.normalize().negate()) { ... }

а також:

for (double x : Vector{1., 2., 3.}) { ... }

Однак наступне (мені здається) порушено:

for (double x : Vector{1., 2., 3.}.normalize()) { ... }

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

  • Це правильно і широко цінується?
  • Яка частина вищезазначеного є «поганою» частиною, якої слід уникати?
  • Чи можна вдосконалити мову, змінивши визначення циклу, заснованого на діапазоні, таким чином, щоб тимчасові структури, побудовані у виразі for, існували протягом усього циклу?

Чомусь я згадую дуже подібне питання, яке вже задавали раніше, але забув, як його називали.
Pubby

Я вважаю це дефектом мови. Життя тимчасових виробників не поширюється на все тіло циклу for, а лише для налаштування циклу for. Страждає не лише синтаксис діапазону, страждає і класичний синтаксис. На мою думку, життя тимчасових людей у ​​заяві init має продовжуватися на весь термін служби циклу.
edA-qa mort-ora-y

1
@ edA-qamort-ora-y: Я схильний погодитися з тим, що тут ховається невеликий мовний дефект, але я думаю, що це конкретно той факт, що продовження життя відбувається неявно, коли ви безпосередньо прив'язуєте тимчасове до посилання, але не в будь-якому інша ситуація - це здається напівзапеченим вирішенням основної проблеми тимчасового життя, хоча це не означає, що очевидно, що було б кращим рішенням. Можливо, явний синтаксис "продовження життя" при побудові тимчасового, що робить його тривалим до кінця поточного блоку - що ви думаєте?
ndkrempel

@ edA-qamort-ora-y: ... це означає те саме, що прив’язати тимчасове до посилання, але має ту перевагу, що є більш чітким для читача, що відбувається «врізання життя», вбудоване (у виразі , а не вимагати окремої декларації), і не вимагати від вас імені тимчасового.
ndkrempel

Відповіді:


64

Це правильно і широко цінується?

Так, ваше розуміння речей правильне.

Яка частина вищезазначеного є «поганою» частиною, якої слід уникати?

Погана частина - це посилання на значення l на тимчасове повернення з функції та прив’язка його до посилання на значення r. Це так само погано, як це:

auto &&t = Vector{1., 2., 3.}.normalize();

Термін служби тимчасового Vector{1., 2., 3.}не може бути продовжений, оскільки компілятор не уявляє, що повернене значення з normalizeнього посилається.

Чи можна вдосконалити мову, змінивши визначення циклу, заснованого на діапазоні, таким чином, щоб тимчасові структури, побудовані у виразі for, існували протягом усього циклу?

Це було б суперечливо з тим, як працює C ++.

Чи не завадило б це певним прийомам, зробленим людьми, використовуючи прив’язані вирази до тимчасових виробів чи різні методи ледачої оцінки виразів? Так. Але це також вимагає спеціального коду компілятора, а також бентежить, чому він не працює з іншими конструкціями виразів.

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

Зараз (якщо компілятор підтримує це), ви можете зробити так, щоб normalize не можна було викликати тимчасово:

struct Vector {
  double x, y, z;
  // ...
  Vector &normalize() & {
     double s = 1./sqrt(x*x+y*y+z*z);
     x *= s; y *= s; z *= s;
     return *this;
  }
  Vector &normalize() && = delete;
};

Це призведе Vector{1., 2., 3.}.normalize()до помилки компіляції, тоді як v.normalize()буде працювати нормально. Очевидно, ви не зможете робити такі речі, як це:

Vector t = Vector{1., 2., 3.}.normalize();

Але ви також не зможете вчинити неправильно.

Як варіант, як пропонується у коментарях, ви можете зробити так, щоб посилальна версія rvalue повертала значення, а не посилання:

struct Vector {
  double x, y, z;
  // ...
  Vector &normalize() & {
     double s = 1./sqrt(x*x+y*y+z*z);
     x *= s; y *= s; z *= s;
     return *this;
  }
  Vector normalize() && {
     Vector ret = *this;
     ret.normalize();
     return ret;
  }
};

Якби Vectorтип був із реальними ресурсами для переміщення, ви могли б використовувати його Vector ret = std::move(*this);. Оптимізація іменованої віддачі робить це досить оптимальним з точки зору продуктивності.


1
Річ, яка могла б зробити це більшою мірою "зрозумілою", полягає в тому, що новий цикл for синтаксично приховує той факт, що прив'язка посилань відбувається під обкладинками - тобто вона набагато менш відверта, ніж ваші "настільки ж погані" приклади вище. Ось чому здавалося правдоподібним запропонувати правило додаткового продовження терміну служби, лише для нового циклу for.
ndkrempel

1
@ndkrempel: Так, але якщо ви збираєтесь запропонувати мовну функцію, щоб виправити це (і, отже, доведеться почекати до 2017 року), я б віддав перевагу, якби це було всебічніше, щось, що могло б вирішити проблему тимчасового розширення скрізь .
Nicol Bolas,

3
+1. На останньому підході замість того, щоб deleteви могли запропонувати альтернативну операцію, яка повертає значення rvalue: Vector normalize() && { normalize(); return std::move(*this); }(я вважаю, що виклик normalizeусередині функції надішле перевантаження lvalue, але хтось повинен це перевірити :)
Девід Родрігес - dribeas

3
Я ніколи не бачив цього &/ &&кваліфікації методів. Це з C ++ 11 чи це якесь (можливо, широко поширене) запатентоване розширення компілятора. Дає цікаві можливості.
Крістіан Рау,

1
@ChristianRau: Це нове для C ++ 11 і є аналогом C ++ 03 "const" та "volatile" кваліфікації нестатичних функцій-членів, оскільки воно певним чином визначає "це". g ++ 4.7.0, однак, цього не підтримує.
ndkrempel

25

для (подвійний x: Вектор {1., 2., 3.}. normalize ()) {...}

Це не обмеження мови, а проблема з вашим кодом. Вираз Vector{1., 2., 3.}створює тимчасовий, але normalizeфункція повертає посилання lvalue . Оскільки вираз є значенням l , компілятор припускає, що об'єкт буде живим, але оскільки це посилання на тимчасовий, об'єкт вмирає після обробки повного виразу, тому вам залишається бовтається посилання.

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


1
Чи constподовжує посилання термін служби об'єкта в цьому випадку?
Девід Стоун

5
Що порушить чітко бажану семантику normalize()як мутуючої функції існуючого об’єкта. Таким чином питання. Те, що тимчасовий має "тривалий термін життя", коли використовується для конкретної мети ітерації, а не інакше, я вважаю заплутаною помилкою.
Енді Росс,

2
@AndyRoss: Чому? Будь-яке тимчасове прив’язане до посилання на значення r (або const&) продовжує своє життя.
Нікол Болас

2
@ndkrempel: Тим НЕ менше, не є обмеження діапазону на основі циклу, і те ж питання прийде , якщо ви пов'язуєте до заслання: Vector & r = Vector{1.,2.,3.}.normalize();. У вашому дизайні є це обмеження, і це означає, що або ви готові повернути за значенням (що може мати сенс у багатьох обставинах, і тим більше з rvalue-посиланнями та переміщенням ), або вам потрібно вирішити проблему на місці виклик: створіть відповідну змінну, а потім використовуйте її у циклі for. Також зверніть увагу, що вираз Vector v = Vector{1., 2., 3.}.normalize().negate();створює два об’єкти ...
Девід Родрігес - дріба

1
@ DavidRodríguez-dribeas: проблема прив'язки до const-reference полягає в наступному: T const& f(T const&);цілком нормально. T const& t = f(T());цілком нормально. А потім, в іншому ТУ, ви виявляєте це T const& f(T const& t) { return t; }і плачете ... Якщо operator+оперує цінностями, це безпечніше ; тоді компілятор може оптимізувати копію (Хочете швидкість? Передати значення), але це бонус. Єдиним прив'язуванням тимчасових пристроїв, яке я б дозволив, є прив'язка до посилань r-значень, але тоді функції повинні повертати значення для безпеки і покладатися на Copy Elision / Move Semantics.
Матьє М.,

4

ІМХО, другий приклад вже недосконалий. Те, що модифікуючі оператори повертають *this, зручно тим способом, про який ви згадали: це дозволяє ланцюжок модифікаторів. Він може бути використаний для простої передачі результату модифікації, але це робить схильним до помилок, оскільки це легко можна пропустити. Якщо я бачу щось подібне

Vector v{1., 2., 3.};
auto foo = somefunction1(v, 17);
auto bar = somefunction2(true, v, 2, foo);
auto baz = somefunction3(bar.quun(v), 93.2, v.qwarv(foo));

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

auto normalized(Vector v) -> Vector {return v.normalize();}
auto negated(Vector v) -> Vector {return v.negate();}

а потім напишіть цикли

for( double x : negated(normalized(v)) ) { ... }

і

for( double x : normalized(Vector{1., 2., 3}) ) { ... }

Це IMO краще читається, і це безпечніше. Звичайно, для цього потрібна додаткова копія, однак для даних, розподілених до купи, це, швидше за все, може бути зроблено в дешевій операції переміщення C ++ 11.


Дякую. Як завжди, існує безліч варіантів вибору. Однією з ситуацій, коли ваша пропозиція може бути нежиттєздатною, є, наприклад, Vector - це масив (не виділена купа) з 1000 подвійних, наприклад. Компроміс ефективності, простоти використання та безпеки використання.
ndkrempel

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