Почему бы не всегда присваивать возвращаемые значения константной ссылке?

Допустим, у меня есть какая-то функция:

Foo GetFoo(..)
{
  ...
}

Предположим, что мы не знаем ни то, как эта функция реализована, ни внутренности Foo (например, это может быть очень сложный объект). Однако мы знаем, что функция возвращает Foo по значению, и что мы хотим использовать это возвращаемое значение как const.

Вопрос: Всегда ли полезно хранить возвращаемое значение этой функции как const &?

const Foo& f = GetFoo(...);

вместо,

const Foo f = GetFoo(...);

Я знаю, что компиляторы будут выполнять оптимизацию возвращаемого значения и могут перемещать объект, а не копировать его в конце const & может не иметь никаких преимуществ. Однако мой вопрос, есть ли недостатки? Почему я не должен просто развивать мышечную память, чтобы всегда использовать const & хранить возвращаемые значения, учитывая, что мне не нужно полагаться на оптимизацию компилятора и тот факт, что даже операция перемещения может быть дорогостоящей для сложных объектов.

Растягивая это до крайности, почему я не должен всегда использовать const & для всех переменных, которые являются неизменными в моем коде? Например,

const int& a = 2;
const int& b = 2;
const int& c = c + d;

Помимо того, что более многословно, есть ли недостатки?

4 ответа

Решение

У них есть семантические различия, и если вы попросите что-то другое, чем хотите, у вас будут проблемы, если вы это получите. Рассмотрим этот код:

#include <stdio.h>

class Bar
{
    public:
    Bar() { printf ("Bar::Bar\n"); }
    ~Bar() { printf ("Bar::~Bar\n"); }
    Bar(const Bar&) { printf("Bar::Bar(const Bar&)\n"); }
    void baz() const { printf("Bar::Baz\n"); }
};

class Foo
{
    Bar bar;

    public:
    Bar& getBar () { return bar; }
    Foo() { }
};

int main()
{
    printf("This is safe:\n");
    {
        Foo *x = new Foo();
        const Bar y = x->getBar();
        delete x;
        y.baz();
    }
    printf("\nThis is a disaster:\n");
    {
        Foo *x = new Foo();
        const Bar& y = x->getBar();
        delete x;
        y.baz();
    }
    return 0;
}

Выход:

Это безопасно:
Бар:: Бар
Бар:: Бар (const Bar &)
Bar::~Bar
Бар::Baz
Bar::~Bar

Это катастрофа:
Бар:: Бар
Bar::~Bar
Бар::Baz

Обратите внимание, мы звоним Bar::Baz после Bar уничтожен К сожалению.

Спросите, что вы хотите, чтобы вы не облажались, если получили то, что просили.

Называть elision "оптимизацией" - заблуждение. Компиляторам разрешено не делать этого, но им также разрешено реализовывать a+b целочисленное сложение как последовательность побитовых операций с ручным переносом.

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

Elision не похожа на "другие" оптимизации, так как те полагаются на правило "как будто" (поведение может изменяться, если оно ведет себя так, как будто стандарт диктует). Elision может изменить поведение кода.

Что касается использования const & или даже значение && плохая идея, ссылки являются псевдонимами объекта. С любым из них у вас нет (локальной) гарантии, что объектом не будут манипулировать в другом месте. На самом деле, если функция возвращает &, const& или же &&объект должен существовать в другом месте с другой идентичностью на практике. Таким образом, ваше "локальное" значение вместо этого является ссылкой на какое-то неизвестное отдаленное состояние: это затрудняет рассуждение о локальном поведении.

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

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

Чтобы быть конкретным:

Foo const& f = GetFoo();

может быть привязкой ссылки к временному типу Foo или полученный вернулся из GetFoo()или ссылка, привязанная к чему-то еще, хранящемуся внутри GetFoo(), Мы не можем сказать из этой линии.

Foo const& GetFoo();

против

Foo GetFoo();

делать f имеют разные значения, в действительности.

Foo f = GetFoo();

всегда создает копию. Ничего такого, что не меняет "через" f будет изменять f (конечно, если его ctor не передал указатель на себя кому-то еще).

Если у нас есть

const Foo f = GetFoo();

у нас даже есть гарантия, что изменение (неmutable части) f является неопределенным поведением. Мы можем предположить f является неизменным, и фактически компилятор сделает это.

в const Foo& кейс, модифицирующий f можно определить поведение, если основное хранилище былоconst, Таким образом, мы не можем предположить, f является неизменным, и компилятор будет предполагать, что он неизменен, только если он может исследовать весь код, который имеет корректно полученные указатели или ссылки на f и определите, что никто из них не мутирует его (даже если вы просто const Foo&если исходный объект был неconst Фу, это законно const_cast<Foo&> и изменить его).

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

Основываясь на том, что David Schwartz сказал в комментариях, вы должны быть уверены, что семантика не изменится. Недостаточно того, что вы намереваетесь рассматривать значение как неизменяемое, функция, из которой вы получили его, должна также обрабатывать его как неизменяемое, иначе вас ждет сюрприз.

image.SetPixel(x, y, white_pixel);
const Pixel &pix = image.GetPixel(x, y);
image.SetPixel(x, y, black_pixel);
cout << pix;

Семантическая разница между const C& а также const C в рассматриваемом случае (при выборе типа для переменной) может повлиять ваша программа в случаях, перечисленных ниже. Они должны учитываться не только при написании нового кода, но и во время последующего обслуживания, поскольку некоторые изменения в исходном коде могут изменяться в зависимости от определения переменной в этой классификации.

Инициализатор является lvalue точно типа C

const C& foo();
const C  a = foo(); // (1)
const C& b = foo(); // (2)

(1) вводит независимый объект (в той степени, которая допускается семантикой копирования типа C), тогда как (2) создает псевдоним другого объекта и подвержен всем изменениям, происходящим с этим объектом (включая его окончание срока службы).

Initializer - это lvalue типа, полученного из C

struct D : C { ... };
const D& foo();
const C  a = foo(); // (1)
const C& b = foo(); // (2)

(1) является нарезанной версией того, что было возвращено из foo(), (2) привязан к производному объекту и может пользоваться преимуществами полиморфного поведения (если оно есть), хотя и рискует быть укушенным из-за проблем с псевдонимами.

Initializer - это значение типа, производного от C

struct D : C { ... };
D foo();
const C  a = foo(); // (1)
const C& b = foo(); // (2)

Для (1) это ничем не отличается от предыдущего случая. Что касается (2), больше нет псевдонимов! Ссылка на константу привязана к временному производного типа, время жизни которого продолжается до конца охватывающей области, с правильным деструктором (~D()) автоматически вызывается. (2) может пользоваться преимуществами полиморфизма, но платит цену за дополнительные ресурсы, потребляемые D по сравнению с C,

Initializer - это r-значение типа, преобразуемого в l-значение типа C

struct B {
    C c;
    operator const C& () const { return c; }
};
const B foo();
const C  a = foo(); // (1)
const C& b = foo(); // (2)

(1) делает свою копию и продолжает работу, в то время как (2) испытывает затруднения, начиная сразу со следующего оператора, так как он псевдоним подчиненного объекта мертвого объекта!

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