Подтвердить что ты не робот

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

Скажем, у меня есть некоторая функция:

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;

Помимо более подробных, есть ли недостатки?

4b9b3361

Ответ 1

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

#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;
}

Выход:

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

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

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

Спросите, чего вы хотите, таким образом, вы не ввернуты, если получите то, о чем попросите.

Ответ 2

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

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

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

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

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

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

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

Foo const& f = GetFoo();

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

Foo const& GetFoo();

против

Foo GetFoo();

make f имеют по-разному значения.

Foo f = GetFoo();

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

Если мы имеем

const Foo f = GetFoo();

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

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

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

Ответ 3

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

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

Ответ 4

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

Инициализатор - это lvalue точно типа C

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

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

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

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

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

Инициализатор - это rvalue типа, полученного из C

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

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

Инициализатор - это rvalue типа, конвертируемого в lvalue типа 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) находится в беде, начинающемся сразу же из следующего утверждения, поскольку он псевдонизирует под-объект мертвого объекта!