Безопасно ли возвращать * это как ссылку?

Возвращаемая ссылка на этот объект часто используется при перегрузке оператора присваивания. Он также используется в качестве базы для именованных параметров идиома, которая позволяет инициализировать объект по цепочке обращений к методам установки: Params().SetX(1).SetY(1) каждый из которых возвращает ссылку на *this.

Но правильно ли возвращать ссылку на *this, Что если мы вызовем метод, возвращающий ссылку на это для временного объекта:

#include <iostream>

class Obj
{
public:
    Obj(int n): member(n) {}
    Obj& Me() { return *this; }

    int member;
};

Obj MakeObj(int n)
{
    return Obj(n);
}

int main()
{
    // Are the following constructions are correct:
    std::cout << MakeObj(1).Me().member << std::endl;
    std::cout << Obj(2).Me().member << std::endl;
    Obj(3).Me() = Obj(4);

    return 0;
}

3 ответа

Решение

Да, это безопасно вернуть * это. Простой случай, когда это не временно, хотя даже тогда, когда это так, это должно быть возможно:

Временные объекты уничтожаются как последний шаг в оценке полного выражения (1.9), которое (лексически) содержит точку, где они были созданы. Это верно, даже если эта оценка заканчивается генерацией исключения (C++03 §12.2/3).

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

Так что следующий код должен работать:

std::cout << MakeObj(1).Me().member << std::endl;

Пока это не должно работать:

const Obj &MakeMeObj(int n) { return Obj(n).Me(); }
std::cout << MakeMeObj(1).member << std::endl;

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

Лично я бы запретил вызывать эти методы для временного объекта, чтобы заставить пользователей API думать о времени жизни объекта. Что можно сделать, перегрузив ваш метод: (если ваш компилятор уже поддерживает его)

Obj &Me() & { return *this; }
Obj &Me() && = delete;
// Are the following constructions are correct:
std::cout << MakeObj(1).Me().member << std::endl;
std::cout << Obj(2).Me().member << std::endl;

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

Как говорит cppreference.com:

(...) все временные объекты уничтожаются как последний шаг в оценке полного выражения, которое (лексически) содержит точку, где они были созданы (...).

Если вы попытаетесь разделить полное выражение, то (надеюсь) вы получите ошибку или предупреждение компилятора:

// not allowed:
Obj& ref = MakeObj(1);
std::cout << ref.Me().member << std::endl;

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

// undefined behaviour:
Obj &ref = MakeObj(1).Me();
std::cout << ref.member << std::endl;

Да, это безопасно. Срок действия временного объекта будет до конца оператора (точнее, оценки полного выражения, в котором он создан). Это гарантируется стандартом:

12.2 / 3: Временные объекты уничтожаются как последний шаг в оценке полного выражения, которое (лексически) содержит точку, где они были созданы.

Временное время жизни может даже быть продлено при некоторых условиях, если привязано к ссылке. Но не ждите чудес здесь. Попытка сохранить ссылку за пределами оператора (например, взяв адрес или назначив ссылку) может быстро привести к UB ( демо).

Если бы вы использовали этот вид конструкции на const объекты у вас также будут некоторые проблемы, как вы пытаетесь вернуть не const ref (но в ваших примерах это не относится к заданию и установщикам).

Другие вопросы по тегам