Представим, что у нас есть структура для хранения 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, существовали в течение всего цикла?
Ответы:
Да, вы правильно понимаете.
Плохая часть - это взять ссылку l-значения на временное значение, возвращаемое функцией, и привязать его к ссылке на r-значение. Это так же плохо:
auto &&t = Vector{1., 2., 3.}.normalize();
Время жизни временного
Vector{1., 2., 3.}
не может быть продлено, поскольку компилятор не знает, что возвращаемое значениеnormalize
ссылается на него.Это было бы очень несовместимо с тем, как работает 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);
вместо этого. Оптимизация именованного возвращаемого значения делает это достаточно оптимальным с точки зрения производительности.источник
delete
вы могли предоставить альтернативную операцию, которая возвращает rvalue:Vector normalize() && { normalize(); return std::move(*this); }
(я считаю, что вызовnormalize
внутри функции отправит перегрузку lvalue, но кто-то должен это проверить :)&
/&&
квалификации методов. Это из C ++ 11 или это какое-то (возможно, широко распространенное) проприетарное расширение компилятора. Дает интересные возможности.Это не ограничение языка, а проблема вашего кода. Выражение
Vector{1., 2., 3.}
создает временный объект, ноnormalize
функция возвращает ссылку на lvalue . Поскольку выражение является lvalue , компилятор предполагает, что объект будет живым, но поскольку это ссылка на временный объект, объект умирает после вычисления полного выражения, поэтому остается висящая ссылка.Теперь, если вы измените свой дизайн, чтобы возвращать новый объект по значению, а не по ссылке на текущий объект, тогда не будет проблем, и код будет работать, как ожидалось.
источник
const
в этом случае ссылка продлить время жизни объекта?normalize()
функции изменения существующего объекта. Итак, вопрос. То, что временный объект имеет «увеличенный срок жизни» при использовании для конкретной цели итерации, а не иначе, я считаю сбивающей с толку ошибкой.const&
службы любой временной привязки к ссылке (или ) r-значения продлен.Vector & r = Vector{1.,2.,3.}.normalize();
. В вашем дизайне есть это ограничение, а это означает, что либо вы готовы вернуться по значению (что может иметь смысл во многих обстоятельствах, особенно с rvalue-ссылками и перемещением ), либо вам нужно решить проблему на месте вызов: создайте правильную переменную, затем используйте ее в цикле for. Также обратите внимание, что выражениеVector v = Vector{1., 2., 3.}.normalize().negate();
создает два объекта ...T const& f(T const&);
все в порядке.T const& t = f(T());
совершенно нормально. А потом, в другом ЕП, вы это обнаруживаетеT const& f(T const& t) { return t; }
и плачете ... Еслиoperator+
действует по ценностям, это безопаснее ; тогда компилятор может оптимизировать копию (хотите скорости? Передавать по значениям), но это бонус. Единственная привязка временных библиотек, которую я бы позволил, - это привязка к ссылкам на r-значения, но функции должны затем возвращать значения для безопасности и полагаться на Copy Elision / Move Semantics.ИМХО, второй пример уже ошибочен. То, что модифицирующие операторы возвращаются
*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.
источник