Является ли практика возврата ссылочной переменной C++ злой?
Это немного субъективно, я думаю; Я не уверен, что мнение будет единодушным (я видел много фрагментов кода, где возвращаются ссылки).
В соответствии с комментарием к этому вопросу, который я только что спросил, относительно инициализации ссылок, возврат ссылки может быть злым, потому что, [насколько я понимаю], легче пропустить его удаление, что может привести к утечкам памяти.
Это беспокоит меня, так как я следовал примерам (если я не представляю себе что-то) и делал это в нескольких местах... Я неправильно понял? Это зло? Если так, то насколько злой?
Я чувствую, что из-за моего смешанного мешка с указателями и ссылками, в сочетании с тем, что я новичок в C++, и полной путаницы по поводу того, что использовать, когда мои приложения должны быть адом утечки памяти...
Кроме того, я понимаю, что использование интеллектуальных / общих указателей обычно считается лучшим способом избежать утечек памяти.
16 ответов
В общем, возвращение ссылки совершенно нормально и происходит постоянно.
Если ты имеешь ввиду:
int& getInt() {
int i;
return i; // DON'T DO THIS.
}
Это все виды зла. Выделенный стек i
уйдет, а вы ни на что не ссылаетесь. Это тоже зло
int& getInt() {
int* i = new int;
return *i; // DON'T DO THIS.
}
Потому что теперь клиент должен в конечном итоге сделать странное:
int& myInt = getInt(); // note the &, we cannot lose this reference!
delete &myInt; // must delete...totally weird and evil
int oops = getInt();
delete &oops; // undefined behavior, we're wrongly deleting a copy, not the original
Обратите внимание, что ссылки на rvalue по-прежнему являются просто ссылками, поэтому все злые приложения остаются неизменными.
Если вы хотите выделить что-то, что выходит за рамки функции, используйте умный указатель (или вообще контейнер):
std::unique_ptr<int> getInt() {
return std::make_unique<int>(0);
}
И теперь клиент хранит умный указатель:
std::unique_ptr<int> x = getInt();
Ссылки также подходят для доступа к вещам, в которых вы знаете, что время жизни открыто на более высоком уровне, например:
struct immutableint {
immutableint(int i) : i_(i) {}
const int& get() const { return i_; }
private:
int i_;
};
Здесь мы знаем, что можно вернуть ссылку на i_
потому что все, что вызывает нас, управляет временем жизни экземпляра класса, так i_
будет жить по крайней мере так долго.
И, конечно же, нет ничего плохого в том, чтобы просто:
int getInt() {
return 0;
}
Если время жизни должно быть предоставлено вызывающей стороне, а вы просто вычисляете значение.
Резюме: нормально возвращать ссылку, если время жизни объекта не закончится после вызова.
Нет, нет, тысячу раз нет.
Зло заключается в том, что он делает ссылку на динамически размещенный объект и теряет исходный указатель. Когда ты new
объект вы берете на себя обязательство иметь гарантированный delete
,
Но посмотрите, например, operator<<
: это должно вернуть ссылку, или
cout << "foo" << "bar" << "bletch" << endl ;
не сработает
Вы должны вернуть ссылку на существующий объект, который не исчезнет немедленно и в котором вы не собираетесь передавать права собственности.
Никогда не возвращайте ссылку на локальную переменную или что-то подобное, потому что на нее не будет ссылки.
Вы можете вернуть ссылку на что-то независимое от функции, которое, как вы ожидаете, не вызовет функцию, которая возьмет на себя ответственность за удаление. Это случай для типичного operator[]
функция.
Если вы создаете что-то, вы должны вернуть либо значение, либо указатель (обычный или умный). Вы можете возвращать значение свободно, так как оно входит в переменную или выражение в вызывающей функции. Никогда не возвращайте указатель на локальную переменную, так как она исчезнет.
Я считаю, что ответы не являются удовлетворительными, поэтому я добавлю свои два цента.
Давайте проанализируем следующие случаи:
Ошибочное использование
int& getInt()
{
int x = 4;
return x;
}
Это явно ошибка
int& x = getInt(); // will refer to garbage
Использование со статическими переменными
int& getInt()
{
static int x = 4;
return x;
}
Это правильно, потому что статические переменные существуют на протяжении всей жизни программы.
int& x = getInt(); // valid reference, x = 4
Это также довольно часто встречается при реализации шаблона Singleton
Class Singleton
{
public:
static Singleton& instance()
{
static Singleton instance;
return instance;
};
void printHello()
{
printf("Hello");
};
}
Использование:
Singleton& my_sing = Singleton::instance(); // Valid Singleton instance
my_sing.printHello(); // "Hello"
операторы
Контейнеры стандартной библиотеки сильно зависят от использования операторов, которые, например, возвращают ссылку
T & operator*();
может использоваться в следующих
std::vector<int> x = {1, 2, 3}; // create vector with 3 elements
std::vector<int>::iterator iter = x.begin(); // iterator points to first element (1)
*iter = 2; // modify first element, x = {2, 2, 3} now
Быстрый доступ к внутренним данным
Есть моменты, когда & могут быть использованы для быстрого доступа к внутренним данным
Class Container
{
private:
std::vector<int> m_data;
public:
std::vector<int>& data()
{
return m_data;
}
}
с использованием:
Container cont;
cont.data().push_back(1); // appends element to std::vector<int>
cont.data()[0] // 1
ОДНАКО, это может привести к ловушке, такой как это:
Container* cont = new Container;
std::vector<int>& cont_data = cont->data();
cont_data.push_back(1);
delete cont; // This is bad, because we still have a dangling reference to its internal data!
cont_data[0]; // dangling reference!
Это не зло. Как и многие вещи в C++, это хорошо, если используется правильно, но есть много подводных камней, о которых вы должны знать при использовании (например, возвращая ссылку на локальную переменную).
Есть хорошие вещи, которые могут быть достигнуты с ним (например, map[name] = "hello world")
"возвращать ссылку - это зло, потому что просто [как я понимаю] легче пропустить ее удаление"
Не правда. Возврат ссылки не подразумевает семантику владения. То есть только потому, что вы делаете это:
Value& v = thing->getTheValue();
... не означает, что теперь у вас есть память, на которую ссылается v;
Тем не менее, это ужасный код:
int& getTheValue()
{
return *new int;
}
Если вы делаете что-то вроде этого, потому что "вам не нужен указатель на этот экземпляр", то: 1) просто разыменуйте указатель, если вам нужна ссылка, и 2) вам в конечном итоге понадобится указатель, потому что вы должны соответствовать новый с удалением, и вам нужен указатель для вызова удаления.
Есть два случая:
константная ссылка - хорошая идея, иногда, особенно для тяжелых объектов или прокси-классов, оптимизация компилятора
неконстантная ссылка - плохая идея иногда нарушает инкапсуляцию
Оба имеют одну и ту же проблему - потенциально могут указывать на уничтоженный объект...
Я бы порекомендовал использовать умные указатели для многих ситуаций, когда вам требуется возвращать ссылку / указатель.
Также обратите внимание на следующее:
Существует формальное правило - Стандарт C++ (раздел 13.3.3.1.4, если вам интересно) гласит, что временная ссылка может быть связана только с константной ссылкой - если вы пытаетесь использовать неконстантную ссылку, компилятор должен пометить это как ошибка.
Мало того, что это не зло, это иногда важно. Например, было бы невозможно реализовать оператор [] в std::vector без использования возвращаемого ссылочного значения.
Дополнение к принятому ответу:
struct immutableint { immutableint(int i) : i_(i) {} const int& get() const { return i_; } private: int i_; };
Я бы сказал, что этот пример нехорош и его следует избегать, если это возможно. Зачем? Это очень легко закончить висячей ссылкой.
Чтобы проиллюстрировать это на примере:
struct Foo
{
Foo(int i = 42) : boo_(i) {}
immutableint boo()
{
return boo_;
}
private:
immutableint boo_;
};
вход в опасную зону:
Foo foo;
const int& dangling = foo.boo().get(); // dangling reference!
Ссылка на возврат обычно используется в C++ для перегрузки операторов для больших объектов, поскольку для возврата значения требуется операция копирования (при перегрузке perator мы обычно не используем указатель в качестве возвращаемого значения)
Но обратная ссылка может вызвать проблемы с выделением памяти. Поскольку ссылка на результат будет передана из функции в качестве ссылки на возвращаемое значение, возвращаемое значение не может быть автоматической переменной.
если вы хотите использовать возвращающую ссылку, вы можете использовать буфер статического объекта. например
const max_tmp=5;
Obj& get_tmp()
{
static int buf=0;
static Obj Buf[max_tmp];
if(buf==max_tmp) buf=0;
return Buf[buf++];
}
Obj& operator+(const Obj& o1, const Obj& o1)
{
Obj& res=get_tmp();
// +operation
return res;
}
таким образом, вы можете безопасно использовать возвращаемую ссылку.
Но вы всегда можете использовать указатель вместо ссылки для возврата значения в функции g.
Лучше всего создать объект и передать его в качестве параметра ссылки / указателя в функцию, которая выделяет эту переменную.
Выделение объекта в функции и возвращение его в качестве ссылки или указателя (однако указатель безопаснее) является плохой идеей из-за освобождения памяти в конце функционального блока.
Я думаю, что использование ссылки в качестве возвращаемого значения функции намного проще, чем использование указателя в качестве возвращаемого значения функции. Во-вторых, всегда было бы безопасно использовать статическую переменную, к которой относится возвращаемое значение.
Функция как lvalue (иначе говоря, возвращение неконстантных ссылок) должна быть удалена из C++. Это ужасно не интуитивно понятно. Скотт Мейерс хотел min() с таким поведением.
min(a,b) = 0; // What???
что на самом деле не улучшение
setmin (a, b, 0);
Последнее даже имеет больше смысла.
Я понимаю, что функция как lvalue важна для потоков в стиле C++, но стоит отметить, что потоки в стиле C++ ужасны. Я не единственный, кто так думает... насколько я помню, у Александреску была большая статья о том, как сделать лучше, и я считаю, что boost также попытался создать лучший тип безопасного ввода-вывода.
Class Set {
int *ptr;
int size;
public:
Set(){
size =0;
}
Set(int size) {
this->size = size;
ptr = new int [size];
}
int& getPtr(int i) {
return ptr[i]; // bad practice
}
};
Функция getPtr может получить доступ к динамической памяти после удаления или даже к нулевому объекту. Что может привести к недопустимым исключениям доступа. Вместо этого должны быть реализованы метод получения и установки и проверка размера перед возвратом.
Я столкнулся с реальной проблемой, где это было действительно зло. По сути, разработчик вернул ссылку на объект в векторе. Это было плохо!!!
Полная информация, о которой я писал в Janurary: http://developer-resource.blogspot.com/2009/01/pros-and-cons-of-returing-references.html
О ужасном коде:
int& getTheValue()
{
return *new int;
}
Так что, действительно, указатель памяти теряется после возврата. Но если вы используете shared_ptr вот так:
int& getTheValue()
{
std::shared_ptr<int> p(new int);
return *p->get();
}
Память не теряется после возврата и будет освобождена после назначения.