Treat the latest use of automatic storage duration variables as xvalue expressions

Alexandr Timofeev
Alexandr Timofeev

Questions:

* What if a variable of copyable but non-movable type?

While it's considered a defective type it can potentially break some code.

* What if a copy contains side effects?

We already have a wide range of cases when copy-moves are elided and their side effects are ignored. This proposal would make more such cases.

* Should we do it for with references too to support the forwarding use case?

At this point, this proposal explicitly doesn't cover reference parameters. It could make sense for rvalue references, lvalue const references, but not lvalue references.

* Can vendors definitely determine the last use of such a variable?

According to Herb Sutter's latest talk in Prague - yes. 

 

Example:
```
void sample(x_type x, y_type y) {
  process(x);
  if (something(x)) {
    process(y);
    x.hold();    // last use, could be an implicit `std::move(x).hold()`?
  } else {
    cout << x; // last use, could be an implicit `cout << std::move(x)`

  }
  transfer(y);  // last use, could be an implicit `transfer(std::move(y))`
}
```

-5
рейтинг
16 комментариев
yndx-antoshkka

Скорей всего будут примеры кода, которые меняют рантайм поведение из-за этого. Нужно продумать подобные случаи, и показать что уже сейчас они не являются переносимыми.

yndx-antoshkka
Alexandr Timofeev

yndx-antoshkka, так как expressions в return statements не являются xvalue, я уточнил причину. Это сделано из-за старых non-copyable but Cpp17CopyContructable типов. Для поддержки таких типов можно позаимствовать поведение overload resolution из http://eel.is/c++draft/class.copy.elision#3 

Единственное изменение рантайм поведения которое мне приходит в голову это сайд эффекты?

Alexandr Timofeev
Игорь Гусаров

Alexandr Timofeev, отличия будут не только в сайд-эффектах, но ещё и в моменте фактического разрушения объекта:

struct Test
{
    int    value = 3;
};

void bar(shared_ptr<Test>);

void foo(shared_ptr<Test> x, int* ptr)
{
    bar(x);   // This is the last use of `x`.

    // If `x` was implicitly moved to call `bar`,
    // then `Test` object could get destroyed.

    // Question: what if `ptr` was pointing
    // to `Test::value` member of that object?
    *ptr = 56;
}
Игорь Гусаров
Alexandr Timofeev

Игорь Гусаров, действительно, пример:

int main() {
    std::shared_ptr<Test> ptr{};
    auto i = ptr->value; // good
    foo(ptr, &i); // last use of ptr, can move
}

int main() {
    std::shared_ptr<Test> ptr{};
    auto pi = &ptr->value; // oh, no!
    foo(ptr, pi); // last use of ptr? 
}

Фактически, это значит любой вызов мембер функции объекта либо передача объекта по ссылке либо взятие указателя на объект - не дает неявно сделать мув, так как потенциально адрес объекта может быть сохранен во внешнее состояние и компилятор при текущих возможностях не может доказать что на объект ничто не ссылается. 

Это оставляет юз кейз пробрасывания локальной переменной без использования мембер функций либо _только_ по значению, либо, возможно, пробрасывание их в конструктор?

Получилось довольна интересная дискуссия, однако польза выглядит крайне сомнительно относительно проблемных юз кейзов, которые как раз таки наоборот усложнят логику решения где использовать мув явно, а где нет, из-за того что компилятор не может гарантировать что не происходит что-то необычное с адресом локальной переменной.

Alexandr Timofeev
Игорь Гусаров

 

Alexandr Timofeev, простите, что недостаточно чётко озвучил суть того примера: код, рантайм-поведение которого изменится с появлением неявного мува, целиком находится внутри функции foo и, как мне кажется, не зависит от того, применяется ли неявный мув к вызову самой foo.

Но если конкретный способ вызова foo важен, то он может быть таким:

int main()
{
    Test*     p = new Test;
    int*      i = &p->value;
    foo(shared_ptr<Test>{p}, i);
}

int main()
{
    auto      p = make_shared<Test>();
    int*      i = &p->value;
    foo(move(p), i);
}

Повторюсь, мне кажется, что конкретный способ как вызвать функцию так, чтобы разные аргументы ссылались на один и тот же ресурс - не важен. Shared_ptr и функция с двумя аргументами - это только иллюстрация.

Ответ на предположение Антона заключался в том, что при муве RAII-объекта поменяется момент фактического освобождения ресурса: ресурс будет освобождён не в деструкторе исходного, а в деструкторе нового объекта. Что в некоторых случаях может повлиять на логику программы.

Игорь Гусаров
Alexandr Timofeev

Игорь Гусаров, действительно. Я абсолютно упустил такой юз кейз. При этом можно представить несинтетический пример для такой ситуации. Например что-то похожее на идиому shared_from_this в АСИО, только с передачей указателя на текущий объекта в параметрах функций по значению при асинхронных операциях. 

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

Alexandr Timofeev
Игорь Гусаров

Саттер действительно утверждал, что последнее использование автоматического объекта можно спрогнозировать на этапе компиляции? Даже в следующем примере?

void foo(SomeClass x)
{
   bar(x);  // Is this the last use of 'x'?

   int    val;
   std::cin >> val;

   if (val > 5)
       baz(x);
}
Игорь Гусаров
Alexandr Timofeev

Игорь Гусаров, спасибо за прекрасный пример!


Да, Саттер действительно утверждает что это можно сделать в общем случае:
https://youtu.be/qx22oxlQmKc?t=1514

Он ссылается на исследование времени жизни объектов для С++ в https://wg21.link/p1179r1 где приводится глава по поводу ветвления на основе рантайм переменной, более специфично https://godbolt.org/z/WqK1wT .

Так, в вашем примере последнем использованием является только baz(x), что справедливо и для кода написанного человеком. 

 

Alexandr Timofeev
Andrey

Alexandr Timofeev, а как это должно работать в таком случае?

struct X { ... };

struct Y {
  X x1, x2;

  Y(X&&, X&&);  
  Y(X const &, X&&);
  Y(X&&, X const &);
  Y(X const &, X const &);
};

Y f() {
  X x = ...;
  return { x, x };
}
Andrey
Alexandr Timofeev

Andrey, это кстати отличный пример где как раз таки неявные мувы помогут избежать ошибок.

На практике многие эксперты очень часто имеют трудности в том чтобы вспомнить в каких случаях order of evaluation определен. 

Так, в вашем примере используются лист инициализация, порядок определен слева на право - поэтому последнем определенным использованием `x` является последний аргумент, что позволяет нам написать: 

Y f1() {
  X x = X{};
  return { x, std::move(x) }; // Y(X const &, X&&);
}

Однако, в другой ситуации:

Y bar(X x1, X x2) {
  return { std::move(x1), std::move(x2) };
}

Y f2() {
  X x = X{};
  return bar(x, std::move(x)); // Oh, no!
}

Вероятность человеческой ошибки очень велика. Однако компилятор точно знает что раз порядок не определен, то `x` не имеет определенного последнего использования. 

Тут могут возникнуть два изменения поведения:
1. Патологический пример:

struct Y {
  X x1, x2;

  Y(X&&, X&&);  
  Y(X const &, X&&) = delete;
  Y(X&&, X const &);
  Y(X const &, X const &);
};

2. Сайд эффекты

void erase_filesystem();
void backup_filesystem();

struct Y {
  X x1, x2;

  Y(X&&, X&&);  
  Y(X const &, X&&) { erase_filesystem(); }
  Y(X&&, X const &);
  Y(X const &, X const &) { backup_filesystem(); }
};

 

Alexandr Timofeev
Alexandr Timofeev

Andrey, я к сожалению упустил момент что в данной ситуации внутри конструктора мы получим ссылки на один и тот же объект, что может поломать поведение внутри функции.

Alexandr Timofeev
Alexandr Timofeev

Andrey, действительно, следуя http://eel.is/c++draft/res.on.arguments#1.3 , то автоматическое преобразование к xvalue возможно только если в текущем expression переменная биндится не более одного раза. По [p1179] это можно отследить, в том числе даже непрямые зависимости в разделе 2.5.2. 

Опять же, в данном случае механизм неявного преобразования помог бы избежать ошибки тем что он бы учел данное правило. 

Alexandr Timofeev
Alexandr Timofeev

Альтернативная проблема https://godbolt.org/z/BX-DCf

#include <string_view>
#include <string>

std::string_view wrap(std::string_view sv) {
    return sv;
}

extern void consume(std::string);
extern void use(std::string_view);

void foo() {
    auto s = std::string("123");
    auto sv = wrap(s);
    consume(s);
    use(sv);
}



Alexandr Timofeev
Alexandr Timofeev

Продолжая проблему, любое выражение, которое биндит аргумент по ссылке/указателю, которе при этом не является последним выражением в локальной области, препятствует неявным мувам, потому что потенциально значение может сохраниться в глобальный стейт и использоваться в функциях дальше. 

Alexandr Timofeev
Игорь Гусаров

Ещё нужно продумать, чтобы неявное move-конструирование аргуметнов не мешало писать код, предоставляющий strong exception guarantee.

Сейчас при простом вызове функции с передачей аргументов по значению соблюдаются все требования strong exception guarantee:

void bar(SomeObject y);

void foo(SomeObject x)
{
    bar(x);   // If `bar` throws, the value of `x` is not modified.
}

В случае же неявного мува, уже не получится сохранить исходное значение x при вылете исключения из bar.

Игорь Гусаров
Alexandr Timofeev

Игорь Гусаров, с другой стороны, является ли состояние "x" наблюдаемым состоянием программы в случае если только ее последнее использование мувит? Ведь та же классическая имплементация copy-swap делается через параметры по значению.
Приведите пожалуйста более расширенный пример когда мы можем наблюдать нарушение гарантии. 

Alexandr Timofeev
Другие идеи
Группа создана, чтобы собирать предложения к стандарту C++, организовывать их внутренние обсуждения, помогать готовить их для отправки в комитет и защищать на общих собраниях в рабочей группе по С++ Международной организации по стандартизации (ISO).
Все предложения