Правильный способ возврата ссылки rvalue на этот

В приведенном ниже коде содержится Undefined Поведение. Обязательно прочитайте ВСЕ ответы для полноты.


При привязке объекта через operator<< я хочу сохранить lvalue-ness/rvalue-ness объекта:

class Avenger {
  public:
    Avenger& operator<<(int) & {
      return *this;
    }
    Avenger&& operator<<(int) && {
      return *this; // compiler error cannot bind lvalue to rvalue
      return std::move(*this);
    }
};
void doJustice(const Avenger &) {};
void doJustice(Avenger &&) {};

int main() {
  Avenger a;
  doJustice(a << 24); // parameter should be Avenger&
  doJustice(Avenger{} << 24); // parameter should be Avenger&&

  return 0;
}

Я не могу просто вернуть *this, что означает, что тип *this объекта rvalue все еще является lvalue reference. Я ожидал бы rvalue reference.

  • Правильно ли/рекомендуется возвращать std::move(*this) член, перегруженный для квалификатора &&, или использовать другой метод? Я знаю, что std::move - это просто актерский состав, поэтому я думаю, что все в порядке, я просто хочу дважды проверить.
  • В чем причина/объяснение того, что *this для rvalue является lvalue reference, а не rvalue reference?
  • Я помню, что видел в С++ 14 что-то о семантике перемещения *this. Связано ли это с этим? Будет ли любое из приведенных выше изменений в С++ 14?

Ответ 1

Это НЕ нормально, чтобы вернуть std::move(*this) член, перегруженный для квалификатора &&. Проблема здесь не в std::move(*this) (какие другие ответы правильно показывают, что это нормально), но с типом возврата. Проблема очень тонкая, и это почти было сделано комитетом по стандартизации С++ 11. Это объясняется Стефаном Т. Лававием в его презентации Не помогите компилятору во время Going Native 2013. Его пример можно найти около минуты 42 в связанном видео. Его пример немного отличается и не включает *this и использует перегрузку по типу привязки параметра, а не по квалификаторам ref ref, но принцип все тот же.

Итак, что не так с кодом?

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

void foo(std::string const & s) {
  //
}

foo("Temporary std::string object constructed from this char * C-string");

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

Возвращаясь к моему примеру:

Для завершения добавьте функцию, которая принимает только ссылку константы lvalue на Avenger (без перегрузки опорных значений rvalue):

void doInjustice(Avenger const &) {};

следующие два вызова приводят к UB, если ссылаются на параметр внутри функций:

doInjustice(Avenger{} << 24); // calls `void doInustice(Avenger const &) {};`
doJustice(Avenger{} << 24); // calls `void doJustice(Avenger &&) {};` 

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


правильный способ - вернуть по значению:

class Avenger {
  public:
    Avenger& operator<<(int) & {
      return *this;
    }
    Avenger operator<<(int) && {
      return std::move(*this);
    }
};

Копия все еще ускользает из семантики перемещения, а возврат является временным, что означает, что он вызовет правильную перегрузку, но мы избегаем этой тонкой, но противной тихой ошибки.


Пример Stephan T. Lavavej: Не помогите компилятору (42 м-45 м)

string&& join(string&& rv, const char * ptr) {
  return move(rv.append(", ").append(ptr));
}
string meow() { return "meow"; }

const string& r = join(meow(), "purr");
// r refers to a destroyed temporary!

//Fix:
string join(string&& rv, const char * ptr) {
  return move(rv.append(", ").append(ptr));
}

Сообщения на SO, объясняющие продление жизни временных объектов через ссылки:

Ответ 2

Тип this зависит от cv-определителя функции-члена: Avenger* или const Avenger*

Но не на его ref-определителе. Ref-qualifier используется только для определения вызываемой функции.

Таким образом, тип *this равен Avenger& или const Avenger&, независимо от того, используете ли вы && или нет. Разница в том, что будет использоваться перегрузка с помощью &&, тогда вызываемый объект является r-значением, а & не будет.

Обратите внимание, что rvalue-ness является свойством выражения, а не объекта. Например:

void foo(Avenger &x)
{
    foo(x); //recursive call
}
void foo(Avenger &&x)
{
    foo(x); //calls foo(Avenger &)!
}

То есть, хотя во втором foo(), x определяется как ссылка на r-значение, любое использование выражения x все еще является l-значением. То же самое верно для *this.

Итак, если вы хотите вывести объект, return std::move(*this) - это правильный путь.

Возможно, все было иначе: this был определен как ссылочное значение вместо указателя? Я не уверен, но я думаю, что рассмотрение *this как значения r может привести к некоторым безумным ситуациям...

Я ничего не слышал об этом в С++ 14, но я могу ошибаться...

Ответ 3

std::move, пожалуй, лучше назвать rvalue_cast.

Но это не называется. Несмотря на свое название, это не что иное, как rvalue cast: std::move не перемещается.

Все именованные значения являются значениями lvalues, как и все разыменования указателей, поэтому используйте std::move или std::forward (aka условный rvalue cast), чтобы превратить именованное значение, которое является ссылкой rvalue в точке объявления (или по другим причинам) в значение r в определенной точке кошернее.

Обратите внимание, однако, что вы редко хотите вернуть ссылку rvalue. Если ваш тип дешев для перемещения, вы обычно хотите вернуть литерал. Для этого используется те же std::move в теле метода, но теперь он фактически запускает переход в возвращаемое значение. И теперь, если вы зафиксируете возвращаемое значение в ссылке (скажем auto&& foo = expression;), эталонное продление жизни работает правильно. О единственном хорошем времени для возврата ссылки на rvalue в rvalue cast: какой тип делает тот факт, что move является значением rvalue, несколько академическим.

Ответ 4

Этот ответ отвечает на bolov комментарий к нему под его ответом на его вопрос.

#include <iostream>

class Avenger
{
    bool constructed_ = true;

  public:
    Avenger() = default;

    ~Avenger()
    {
        constructed_ = false;
    }

    Avenger(Avenger const&) = default;
    Avenger& operator=(Avenger const&) = default;
    Avenger(Avenger&&) = default;
    Avenger& operator=(Avenger&&) = default;

    Avenger& operator<<(int) &
    {
      return *this;
    }

    Avenger&& operator<<(int) &&
    {
      return std::move(*this);
    }

    bool alive() const {return constructed_;}
};

void
doJustice(const Avenger& a)
{
    std::cout << "doJustice(const Avenger& a): " << a.alive() << '\n';
};

void
doJustice(Avenger&& a)
{
    std::cout << "doJustice(Avenger&& a): " << a.alive() << '\n';
};

int main()
{
  Avenger a;
  doJustice(a << 24); // parameter should be Avenger&
  doJustice(Avenger{} << 24); // <--- this one
  // Avenger&& dangling = Avenger{} << 24;
  // doJustice(std::move(dangling));
}

Это приведет к переносимости вывода:

doJustice(const Avenger& a): 1
doJustice(Avenger&& a): 1

Как показывает вышеприведенный вывод, временный объект Avenger не будет разрушен до тех пор, пока точка последовательности не будет демаркирована символом ';' перед комментарием "//< --- this one" выше.

Я удалил все поведение undefined из этой программы. Это полностью совместимая и портативная программа.

Ответ 5

Моя git копия стандарта С++ (стр. 290, раздел 15.2, пункт 6) гласит:

"Исключения из этого правила жизни [ссылки привязки к временное продление срока службы этого временного]:

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

sub 10 - Время жизни временной привязки к возвращаемому значению            в возврате функции return (9.6.3) не продолжается;            временное уничтожение в конце            full-expression в операторе return. [...]"

Следовательно, неявный этот "параметр" передан в

Avenger&& operator<<(int) &&
{
      return std::move(*this);
}

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

Avenger &&dangler = Avenger{} << 24; // destructor already called
dangler << 1; // Operator called on dangling reference

Если OTOH возвращается по значению:

Avenger operator<<(int) &&
{
      return std::move(*this);
}

то ничто из этого несчастья не происходит, и часто нет никаких дополнительных затрат с точки зрения копирования. И действительно, если вы не предложите компилятору создать это возвращаемое значение путем move-constructing from * this, тогда он сделает копию. Так что вырвите * это с краю забвения и получите лучшее из обоих миров.