Является ли это известной ловушкой С++ 11 для петель?

Предположим, что у нас есть структура для размещения трех пар с некоторыми функциями-членами:

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 в цикле нового стиля, скажем, чтобы зацикливать на 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-expression, существовали на протяжении цикла?

Ответ 1

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

Да, ваше понимание вещей верное.

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

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

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

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

Будет ли улучшен язык путем изменения определения цикла, основанного на диапазоне, для того, чтобы временные структуры, построенные в for-expression, существовали на протяжении цикла?

Это было бы очень несовместимо с тем, как работает С++.

Может ли это предотвратить некоторые ошибки, сделанные людьми, использующими прикомандированные выражения на временных страницах или различные методы ленивой оценки выражений? Да. Но для этого также потребуется специальный код компилятора, а также путайте, почему он не работает с другими конструкциями выражений.

Более разумным решением было бы каким-то образом информировать компилятор о том, что возвращаемое значение функции всегда является ссылкой на 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);. Именованная оптимизация возвращаемого значения делает это достаточно оптимальным с точки зрения производительности.

Ответ 2

для (double x: Vector {1., 2., 3.}. normalize()) {...}

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

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

Ответ 3

ИМХО, второй пример уже испорчен. То, что изменяющие операторы возвращают *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}) ) { ... }

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