Является ли практика возврата ссылочной переменной C ++ злой?

341

Это немного субъективно, я думаю; Я не уверен, что мнение будет единодушным (я видел много фрагментов кода, где возвращаются ссылки).

В соответствии с комментарием к этому вопросу, который я только что спросил, относительно инициализации ссылок , возврат ссылки может быть злым, потому что, [насколько я понимаю], легче пропустить его удаление, что может привести к утечкам памяти.

Это беспокоит меня, так как я следовал примерам (если я не представляю себе что-то) и делал это в нескольких местах ... Я неправильно понял? Это зло? Если так, то насколько злой?

Я чувствую, что из-за моего смешанного мешка с указателями и ссылками, в сочетании с тем, что я новичок в C ++, и полной путаницы в отношении того, что использовать, когда мои приложения должны быть адом утечки памяти ...

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

Ник Болтон
источник
Это не зло, если вы пишете геттер-подобные функции / методы.
Джон З. Ли

Ответы:

411

В общем, возвращение ссылки совершенно нормально и происходит постоянно.

Если ты имеешь ввиду:

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

Если время жизни должно быть предоставлено вызывающей стороне, а вы просто вычисляете значение.

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

GManNickG
источник
21
Это все плохие примеры. Лучший пример правильного использования - когда вы возвращаете ссылку на объект, который был передан. Ala operator <<
Arelius
171
Для потомков и для любых новых программистов, использующих это, указатели неплохие . Также не плохи указатели на динамическую память. Они оба имеют свои законные места в C ++. Интеллектуальные указатели должны определенно быть вашим заданием по умолчанию, когда речь идет о динамическом управлении памятью, но умный указатель по умолчанию должен быть unique_ptr, а не shared_ptr.
Джамин Грей
12
Редактировать утверждающих: не утверждать изменения, если вы не можете ручаться за их правильность. Я откатил неправильное редактирование.
GManNickG
7
Во имя потомства и для любых новых программистов, пишущих об этом, не пишитеreturn new int .
Гонки Легкости на Орбите
3
Ради потомков, и для любых новых программистов, выбирающих это, просто верните T из функции. РВО обо всем позаботится.
Чистка
64

Нет, нет, тысячу раз нет.

Зло - это ссылка на динамически размещенный объект и потеря первоначального указателя. Когда вы newобъект, вы берете на себя обязательство иметь гарантию delete.

Но взгляните, например operator<<: это должно вернуть ссылку, или

cout << "foo" << "bar" << "bletch" << endl ;

не сработает

Чарли Мартин
источник
23
Я высказался против, потому что это ни отвечает на вопрос (в котором OP ясно дал понять, что он знает о необходимости удаления), ни устраняет законный страх, что возврат ссылки на объект freestore может привести к путанице. Вздох.
4
Практика возврата контрольного объекта не является злом. Эрго нет Страх, который он выражает, - это правильный страх, как я отмечаю во втором графике.
Чарли Мартин
Вы на самом деле не сделали. Но это не стоит моего времени.
2
Iraimbilanja @ Про "нет" - мне все равно. но этот пост указал на важную информацию, которая отсутствовала в GMan.
Kobor42
48

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

Никогда не возвращайте ссылку на локальную переменную или что-то подобное, потому что на нее не будет ссылки.

Вы можете вернуть ссылку на что-то независимое от функции, которое, как вы ожидаете, не вызовет функцию, которая возьмет на себя ответственность за удаление. Это касается типичной operator[]функции.

Если вы создаете что-то, вы должны вернуть либо значение, либо указатель (обычный или умный). Вы можете возвращать значение свободно, так как оно входит в переменную или выражение в вызывающей функции. Никогда не возвращайте указатель на локальную переменную, так как она исчезнет.

David Thornley
источник
1
Отличный ответ, но для «Вы можете вернуть временную ссылку как постоянную». Следующий код скомпилируется, но, вероятно, вылетает, потому что временное уничтожается в конце оператора return: "int const & f () {return 42;} void main () {int const & r = f (); ++ r;} "
j_random_hacker
@j_random_hacker: C ++ имеет некоторые странные правила для ссылок на временные ссылки, где временное время жизни может быть увеличено. Извините, я не понимаю это достаточно хорошо, чтобы знать, охватывает ли это ваше дело.
Марк Рэнсом
3
@Mark: Да, у него есть некоторые странные правила. Время жизни временного объекта может быть продлено только путем инициализации константной ссылки (которая не является членом класса) с ним; Затем он живет, пока ссылка не выходит за рамки. К сожалению, возврат const ref не покрывается. Однако возвращение значения temp безопасно.
j_random_hacker
Смотрите стандарт C ++, 12.2, пункт 5. Также см паразитной Гуру Герба Саттера Недели в herbsutter.wordpress.com/2008/01/01/... .
Дэвид Торнли
4
@David: Когда возвращаемый тип функции - «T const &», то, что на самом деле происходит, - то, что оператор return неявно преобразует temp, тип T, в тип «T const &» согласно 6.6.3.2 (допустимое преобразование, кроме одного который не продлевает время жизни), а затем вызывающий код инициализирует ссылку типа «T const &» с результатом функции, также типа «T const &» - опять же, законный, но не продляющий время жизни процесс. Конечный результат: нет продления жизни и много путаницы. :(
j_random_hacker
26

Я считаю, что ответы не являются удовлетворительными, поэтому я добавлю свои два цента.

Давайте проанализируем следующие случаи:

Ошибочное использование

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!
оборота
источник
Возврат ссылки на статическую переменную может привести к нежелательному поведению, например, рассмотрим оператор умножения, который возвращает ссылку на статический член, тогда всегда будет следующее true:If((a*b) == (c*d))
SebNag
Container::data()Реализация должна читатьсяreturn m_data;
Xeaz
Это было очень полезно, спасибо! @Xeaz, не вызовет ли это проблемы с вызовом добавления?
Андрей
@SebTu Зачем тебе это делать?
Торжественный
@ Андрей Нет, это был синтаксис Шенаниган. Если вы, например, вернули тип указателя, то вы использовали бы ссылочный адрес для создания и возврата указателя.
Охотник
14

Это не зло. Как и многие вещи в C ++, это хорошо, если используется правильно, но есть много подводных камней, о которых вы должны знать при использовании (например, возвращая ссылку на локальную переменную).

Есть хорошие вещи, которые могут быть достигнуты с ним (например, map [name] = "hello world")

Мехрдад Афшари
источник
1
Мне просто любопытно, что хорошего map[name] = "hello world"?
неправильное имя
5
@wrongusername Синтаксис интуитивно понятен. Вы когда-нибудь пытались увеличить счетчик значения, хранящегося HashMap<String,Integer>в Java? : P
Мехрдад Афшари
Хаха, пока нет, но, глядя на примеры HashMap, он выглядит довольно мрачно: D
неправильное имя пользователя
Проблема, с которой я столкнулся: функция возвращает ссылку на объект в контейнере, но код вызывающей функции назначил ее локальной переменной. Затем изменились некоторые свойства объекта. Проблема: оригинальный объект в контейнере остался нетронутым. Программист так легко пропускает возвращаемое значение &, и тогда вы получаете действительно неожиданное поведение ...
flohack
10

«возвращать ссылку - это зло, потому что просто [как я понимаю] легче пропустить ее удаление»

Не правда. Возврат ссылки не подразумевает семантику владения. То есть только потому, что вы делаете это:

Value& v = thing->getTheValue();

... не означает, что теперь у вас есть память, на которую ссылается v;

Тем не менее, это ужасный код:

int& getTheValue()
{
   return *new int;
}

Если вы делаете что-то вроде этого, потому что «вам не нужен указатель на этот экземпляр», то: 1) просто разыменуйте указатель, если вам нужна ссылка, и 2) вам в конечном итоге понадобится указатель, потому что вы должны соответствовать новый с удалением, и вам нужен указатель для вызова удаления.

оборота Джон Диблинг
источник
7

Есть два случая:

  • константная ссылка - хорошая идея, иногда, особенно для тяжелых объектов или прокси-классов, оптимизация компилятора

  • неконстантная ссылка - плохая идея иногда нарушает инкапсуляцию

Оба имеют одну и ту же проблему - потенциально могут указывать на уничтоженный объект ...

Я бы порекомендовал использовать умные указатели для многих ситуаций, когда вам требуется возвращать ссылку / указатель.

Также обратите внимание на следующее:

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

Саша
источник
1
неконстантный ref не обязательно нарушает инкапсуляцию. рассмотреть вектор :: оператор []
это очень особый случай ... вот почему я иногда говорил, хотя я действительно должен требовать БОЛЬШЕ ВРЕМЕНИ :)
Итак, вы говорите, что нормальная реализация оператора подписки является необходимым злом? Я не согласен или не согласен с этим; как я не мудрее.
Ник Болтон
Я не говорю этого, но это может быть злым, если злоупотреблять :))) vector :: at следует использовать всегда, когда это возможно ...
а? vector :: at также возвращает неконстантную ссылку.
4

Мало того, что это не зло, это иногда важно. Например, было бы невозможно реализовать оператор [] в std :: vector без использования ссылочного возвращаемого значения.

скоро
источник
Ах да, конечно; Я думаю, именно поэтому я начал использовать его; как когда я впервые реализовал оператор нижнего индекса [], я понял использование ссылок. Я склонен полагать, что это де-факто.
Ник Болтон
Как ни странно, вы можете реализовать operator[]для контейнера без использования ссылки ... и std::vector<bool>делает. (И создает настоящий беспорядок в процессе)
Бен Фойгт
@BenVoigt ммм, почему беспорядок? Возврат прокси-сервера также является допустимым сценарием для контейнеров со сложным хранилищем, которые не отображаются напрямую на внешние типы (как ::std::vector<bool>вы упомянули).
Sergey.quixoticaxis.Ivanov
1
@ Sergey.quixoticaxis.Ivanov: Беспорядок в том, что использование std::vector<T>в шаблоне кода нарушено, если Tможет быть bool, потому std::vector<bool>что поведение очень отличается от других экземпляров. Это полезно, но ему нужно было дать собственное имя, а не специализацию std::vector.
Бен Фойгт
@BenVoight Я согласен с пунктом о странном решении сделать одну специализацию «действительно особенной», но я чувствовал, что ваш оригинальный комментарий подразумевает, что возвращение прокси вообще странно.
Sergey.quixoticaxis.Ivanov
2

Дополнение к принятому ответу:

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!
AMA
источник
1

ссылка на возврат обычно используется в 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.

MinandLucy
источник
0

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

Тони
источник
0

Лучше всего создать объект и передать его в качестве параметра ссылки / указателя в функцию, которая выделяет эту переменную.

Выделение объекта в функции и возврат его в качестве ссылки или указателя (однако указатель безопаснее) является плохой идеей из-за освобождения памяти в конце функционального блока.

Drezir
источник
-1
    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 может получить доступ к динамической памяти после удаления или даже к нулевому объекту. Что может привести к недопустимым исключениям доступа. Вместо этого должны быть реализованы getter и setter и проверен размер перед возвратом.

Amarbir
источник
-2

Функция как lvalue (или возвращение неконстантных ссылок) должна быть удалена из C ++. Это ужасно не интуитивно понятно. Скотт Мейерс хотел min () с таким поведением.

min(a,b) = 0;  // What???

что на самом деле не улучшение

setmin (a, b, 0);

Последнее даже имеет больше смысла.

Я понимаю, что функция как lvalue важна для потоков в стиле C ++, но стоит отметить, что потоки в стиле C ++ ужасны. Я не единственный, кто так думает ... насколько я помню, у Александреску была большая статья о том, как сделать лучше, и я считаю, что boost также попытался создать лучший тип безопасного ввода-вывода.

Дэн Олсон
источник
2
Конечно, это опасно, и должна быть лучшая проверка ошибок компилятора, но без этого нельзя было бы сделать некоторые полезные конструкции, например operator [] () в std :: map.
j_random_hacker
2
Возвращение неконстантных ссылок на самом деле невероятно полезно. vector::operator[]например. Вы бы предпочли написать v.setAt(i, x)или v[i] = x? Последний намного выше.
Майлз Рут
1
@MilesRout я бы пошел на v.setAt(i, x)любое время. Это далеко выше.
Scravy
-2

Я столкнулся с реальной проблемой, где это было действительно зло. По сути, разработчик вернул ссылку на объект в векторе. Это было плохо !!!

Все подробности, о которых я писал в Janurary: http://developer-resource.blogspot.com/2009/01/pros-and-cons-of-returing-references.html

разработкаресурса
источник
2
Если вам нужно изменить исходное значение в вызывающем коде, то вам нужно вернуть ref. И это на самом деле не более и не менее опасно, чем возвращение итератора в вектор - оба становятся недействительными, если элементы добавляются или удаляются из вектора.
j_random_hacker
Эта конкретная проблема была вызвана удержанием ссылки на элемент вектора, а затем изменением этого вектора таким образом, который делает недействительной ссылку: Страница 153, раздел 6.2 «Стандартной библиотеки C ++: Учебное пособие и справочник» - Йосуттис читает: «Вставка или удаление элементов делает недействительными ссылки, указатели и итераторы, которые ссылаются на следующие элементы. Если вставка вызывает перераспределение, она делает недействительными все ссылки, итераторы и указатели "
Трент,
-15

О ужасном коде:

int& getTheValue()
{
   return *new int;
}

Так что, действительно, указатель памяти теряется после возврата. Но если вы используете shared_ptr вот так:

int& getTheValue()
{
   std::shared_ptr<int> p(new int);
   return *p->get();
}

Память не теряется после возврата и будет освобождена после назначения.

Анатолий
источник
12
Он потерян, потому что общий указатель выходит из области видимости и освобождает целое число.
указатель не теряется, адресом ссылки является указатель.
dgsomerton