Является ли это известной ловушкой 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 (), мы могли бы использовать наш вектор в цикле for нового стиля, скажем, чтобы перебрать 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 на основе диапазона таким образом, чтобы временные структуры, созданные в выражении for, существовали в течение всего цикла?
ndkrempel
источник
Почему-то я помню, как раньше задавали очень похожий вопрос, но забыл, как он назывался.
Pubby
Считаю это языковым дефектом. Срок службы временных файлов не распространяется на все тело цикла for, а только на настройку цикла for. Страдает не только синтаксис диапазона, но и классический синтаксис. На мой взгляд, время жизни временных файлов в операторе init должно продлеваться на весь срок жизни цикла.
edA-qa mort-ora-y
1
@ edA-qamort-ora-y: Я склонен согласиться с тем, что здесь скрывается небольшой языковой дефект, но я думаю, что именно тот факт, что продление времени жизни происходит неявно всякий раз, когда вы напрямую привязываете временный объект к ссылке, но не в любом другая ситуация - это похоже на недоработанное решение основной проблемы временных жизней, хотя это не значит, что очевидно, какое было бы лучшее решение. Возможно, явный синтаксис «продления времени жизни» при создании временного, который продлит его до конца текущего блока - как вы думаете?
ndkrempel
@ edA-qamort-ora-y: ... это то же самое, что и привязка временного к ссылке, но имеет то преимущество, что читателю более ясно, что происходит `` продление срока службы '', встроенное вместо того, чтобы требовать отдельного объявления) и не требовать от вас называть временный.
ndkrempel
1
возможный дубликат временного объекта в диапазоне для
ildjarn

Ответы:

64

Это правильно и широко ли это ценится?

Да, вы правильно понимаете.

Какая часть из вышеперечисленного является «плохой», чего следует избегать?

Плохая часть - это взять ссылку l-значения на временное значение, возвращаемое функцией, и привязать его к ссылке на r-значение. Это так же плохо:

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

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

Можно ли улучшить язык, изменив определение цикла for на основе диапазона таким образом, чтобы временные структуры, созданные в выражении 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 года), я бы предпочел, чтобы она была более полной, что-то, что могло бы решить проблему временного расширения повсюду .
Никол Болас
3
+1. При последнем подходе вместо того, чтобы deleteвы могли предоставить альтернативную операцию, которая возвращает rvalue: Vector normalize() && { normalize(); return std::move(*this); }(я считаю, что вызов normalizeвнутри функции отправит перегрузку lvalue, но кто-то должен это проверить :)
Дэвид Родригес - dribeas
3
Я никогда не видел этого &/ &&квалификации методов. Это из C ++ 11 или это какое-то (возможно, широко распространенное) проприетарное расширение компилятора. Дает интересные возможности.
Christian Rau
1
@ChristianRau: Это ново для C ++ 11 и аналогично C ++ 03 "const" и "volatile" квалификациям нестатических функций-членов в том смысле, что в некотором смысле квалифицируется "this". Однако g ++ 4.7.0 не поддерживает его.
ndkrempel
25

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

Это не ограничение языка, а проблема вашего кода. Выражение Vector{1., 2., 3.}создает временный объект, но normalizeфункция возвращает ссылку на lvalue . Поскольку выражение является lvalue , компилятор предполагает, что объект будет живым, но поскольку это ссылка на временный объект, объект умирает после вычисления полного выражения, поэтому остается висящая ссылка.

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

Давид Родригес - дрибеас
источник
1
Может ли constв этом случае ссылка продлить время жизни объекта?
Дэвид Стоун
5
Это нарушило бы явно желаемую семантику normalize()функции изменения существующего объекта. Итак, вопрос. То, что временный объект имеет «увеличенный срок жизни» при использовании для конкретной цели итерации, а не иначе, я считаю сбивающей с толку ошибкой.
Энди Росс
2
@AndyRoss: Почему? Срокconst& службы любой временной привязки к ссылке (или ) r-значения продлен.
Никол Болас
2
@ndkrempel: Тем не менее, не является ограничение диапазона на основе цикла, и тот же вопрос придет , если вы связываете к ссылке: Vector & r = Vector{1.,2.,3.}.normalize();. В вашем дизайне есть это ограничение, а это означает, что либо вы готовы вернуться по значению (что может иметь смысл во многих обстоятельствах, особенно с rvalue-ссылками и перемещением ), либо вам нужно решить проблему на месте вызов: создайте правильную переменную, затем используйте ее в цикле for. Также обратите внимание, что выражение Vector v = Vector{1., 2., 3.}.normalize().negate();создает два объекта ...
Дэвид Родригес - dribeas
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.
Matthieu M.
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}) ) { ... }

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

слева
источник
Спасибо. Как обычно, есть много вариантов. Одна из ситуаций, когда ваше предложение может быть нежизнеспособным, - это, например, если вектор представляет собой массив (не выделенный кучей) из 1000 двойников. Компромисс эффективности, простоты использования и безопасности использования.
ndkrempel
2
Да, но в любом случае редко бывает полезно иметь в стеке структуры размером> ≈100.
leftaround около