Является ли практика возврата ссылочной переменной 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();
}

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

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