Почему бы не всегда присваивать возвращаемые значения константной ссылке?
Скажем, у меня есть некоторая функция:
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;
Помимо более подробных, есть ли недостатки?
Ответы
Ответ 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) находится в беде, начинающемся сразу же из следующего утверждения, поскольку он псевдонизирует под-объект мертвого объекта!