Чи практика повернення контрольної змінної 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

Зауважте, що посилання на оцінку досі залишаються лише посиланнями, тому всі злі програми залишаються однаковими.

Якщо ви хочете виділити щось, що живе за межами функції, використовуйте розумний покажчик (або взагалі контейнер):

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

Якщо термін експлуатації слід залишити для абонента, і ви просто обчислюєте значення.

Підсумок: нормально повернути посилання, якщо термін експлуатації об'єкта не закінчиться після виклику.


21
Це все погані приклади. Найкращий приклад правильного використання - це коли ви повертаєте посилання на переданий об’єкт. Ала-оператор <<
Ареліус

171
Заради нащадків та для будь-яких нових програмістів, які заперечують про це, покажчики не є поганими . Ні вказівники на динамічну пам'ять погані. Вони обоє мають свої законні місця в C ++. Інтелектуальні покажчики, безумовно, повинні стати вашим перемиканням за замовчуванням, якщо мова йде про динамічне управління пам’яттю, але ваш смарт-покажчик за замовчуванням повинен бути унікальним_ptr, а не спільним_ptr.
Джамін Грей

12
Редагувати затверджуючі: не схвалюйте правки, якщо ви не можете порушити його правильність. Я відкинув неправильне редагування.
GManNickG

7
Заради нащадків та для нових програмістів, які заперечують про це, не пишітьreturn new int .
Гонки легкості на орбіті

3
Заради нащадків та для нових программістів, які зазначають це, просто поверніть T з функції. RVO подбає про все.
Взуття

64

Ні, ні, тисяча разів ні.

Що таке зло - це посилання на динамічно виділений об'єкт і втрачає початковий покажчик. Коли ви newоб'єкт, ви берете на себе зобов'язання мати гарантію delete.

Але подивіться, наприклад, що operator<<: повинно повернути посилання, або

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

не буде працювати.


23
Я заявив, що це не дає відповіді на запитання (в якому ОП дав зрозуміти, що він знає необхідність видалення), а також не вирішує законного побоювання, що повернення посилання на об'єкт безкоштовного зберігання може призвести до плутанини. Зітхнути.

4
Практика повернення опорного об’єкта не є злою. Ерго ні. Страх, який він висловлює, - це правильний страх, як я зазначаю у другому графіку.
Чарлі Мартін

Ви насправді не зробили. Але це не варто мого часу.

2
Iraimbilanja @ Про "Ні" -то мені все одно. але ця посада вказала на важливу інформацію, якої не було у GMan.
Кобор42

48

Ви повинні повернути посилання на існуючий об’єкт, який не минає негайно, і де ви не маєте наміру передати право власності.

Ніколи не повертайте посилання на локальну змінну чи якусь подібну, тому що вона не буде там, на яку має бути посилання.

Ви можете повернути посилання на щось незалежне від функції, яке ви не очікуєте, що функція виклику візьме на себе відповідальність за видалення. Це стосується типової operator[]функції.

Якщо ви щось створюєте, вам слід повернути значення або вказівник (звичайний або розумний). Ви можете повернути значення вільно, оскільки воно переходить у змінну або вираз у викличній функції. Ніколи не повертайте вказівник на локальну змінну, оскільки вона піде.


1
Відмінна відповідь, але за те, що "Ви можете повернути тимчасове в якості посилання const". Наступний код буде скомпільований, але, ймовірно, збій, оскільки тимчасовий знищується в кінці оператора return: "int const & f () {return 42;} void main () {int const & r = f (); ++ r;} "
j_random_hacker

@j_random_hacker: C ++ має деякі дивні правила щодо посилань на тимчасові компанії, де тимчасовий термін може бути продовжений. Вибачте, я не розумію цього досить добре, щоб знати, чи охоплює він вашу справу.
Марк Викуп

3
@Mark: Так, у неї є деякі дивні правила. Термін експлуатації тимчасового можна продовжити лише шляхом ініціалізації посилання на const (який не є членом класу) з ним; Потім він живе до тих пір, поки рефлекс не вийде за межі. На жаль, повернення const ref не покривається. Повернення темпу за значенням є безпечним.
j_random_hacker

Дивіться стандарт C ++, 12.2, параграф 5. Також дивіться блукаючого гуру Тижня Герб Саттера на сайті rawutter.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 Чому б ти хотів це зробити?
thorhunter

@Andrew Ні, це був синтаксис шенаніган. Якщо ви, наприклад, повернули тип вказівника, то для створення та повернення вказівника ви використовуєте довідкову адресу.
thorhunter

14

Це не зло. Як і багато речей у C ++, це добре, якщо використовувати їх правильно, але є багато підводних каменів, про які слід пам’ятати (наприклад, повертаючи посилання на локальну змінну).

Є хороші речі, які можна досягти за допомогою нього (наприклад, карта [name] = "привіт світ")


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

Є два випадки:

  • const reference - хороша ідея, іноді, особливо для важких об'єктів або проксі-класів, оптимізація компілятора

  • non-const reference - ідея іноді порушує інкапсуляцію

Обидва поділяють одну проблему - потенційно можуть вказувати на знищений об’єкт ...

Я рекомендую використовувати смарт-покажчики для багатьох ситуацій, коли вам потрібно повернути посилання / покажчик.

Також зверніть увагу на таке:

Існує офіційне правило - стандарт C ++ (якщо ви зацікавлені у розділі 13.3.3.1.4) зазначає, що тимчасове може бути прив'язане лише до посилання const - якщо ви спробуєте використати посилання non-const, компілятор повинен позначити це як помилка.


1
non-const ref не обов'язково порушує інкапсуляцію. розгляньте вектор :: operator []

це дуже особливий випадок ... тому я іноді говорив, хоча мені справді потрібно стверджувати НАЙБІЛЬШЕ ЧАС :)

Отже, ви говорите про те, що нормальна реалізація оператора підписки необхідна зла? Я ні з цим не згоден і не згоден; як я не мудріший.
Нік Болтон

Я цього не кажу, але це може бути злом, якщо неправомірно використовувати :))) вектор :: at слід використовувати, коли це можливо ....

так? vector :: at також повертає nonconst ref.

4

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


Ага, так, звичайно; Я думаю, саме тому я почав його використовувати; як коли я вперше реалізував оператор підписки [], я зрозумів використання посилань. Я думаю, що це фактично.
Нік Болтон

Як не дивно, ви можете реалізувати operator[]контейнер без використання посилання ... і std::vector<bool>так. (І створює справжній безлад у процесі)
Бен Фогт

@BenVoigt ммм, чому безлад? Повернення проксі-сервера також є дійсним сценарієм для контейнерів зі складним сховищем, які не відображаються безпосередньо на зовнішні типи (як ::std::vector<bool>ви вже згадували).
Сергій.quixoticaxis.Ivanov

1
@ Sergey.quixoticaxis.Ivanov: Безлад у тому, що використання std::vector<T>коду шаблону порушено, якщо це Tможливо bool, тому що std::vector<bool>має дуже іншу поведінку від інших інстанцій. Це корисно, але йому слід було б дати власне ім'я, а не спеціалізацію std::vector.
Ben Voigt

@BenVoight Я погоджуюся з думкою про дивне рішення зробити одну спеціалізацію "дійсно особливою", але я відчув, що ваш оригінальний коментар передбачає, що повернення проксі в цілому є дивним.
Сергій.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!

1

Посилання на повернення зазвичай використовується при перевантаженні оператора в C ++ для великого Об'єкта, оскільки для повернення значення потрібна операція копіювання.

Але повернення посилання може спричинити проблеми з розподілом пам'яті. Оскільки посилання на результат буде передано з функції як посилання на повернене значення, повернене значення не може бути автоматичною змінною.

якщо ви хочете скористатися поверненням, ви можете використовувати буфер статичного об'єкта. наприклад

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

таким чином, ви можете безпечно використовувати повернення посилання.

Але ви завжди можете використовувати покажчик замість посилання для повернення значення у functiong.


0

Я думаю, що використання посилання як поверненого значення функції набагато прямо вперед, ніж використання вказівника як зворотного значення функції. По-друге, завжди було б безпечно використовувати статичну змінну, на яку посилається повернене значення.


0

Найкраще створити об’єкт і передати його як параметр / параметр вказівника функції, яка виділяє цю змінну.

Виділення об'єкта у функції та повернення його як опорного чи вказівного (вказівник проте безпечніше) є поганою ідеєю через звільнення пам'яті в кінці функціонального блоку.


-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 повинні бути реалізовані та перевірені розміром перед поверненням.


-2

Функцію як lvalue (він же повертає неконст-посилання) слід видалити з C ++. Це страшенно неінтуїтивно. Скотт Мейерс хотів хвилини () з такою поведінкою.

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

що насправді не покращення

setmin (a, b, 0);

Останнє навіть має більше сенсу.

Я розумію, що функція lvalue важлива для потоків у стилі C ++, але варто зазначити, що потоки стилів C ++ є жахливими. Я не єдиний, хто думає про це ... як я пам'ятаю, в Олександреску була велика стаття про те, як зробити краще, і я вважаю, що стимул також намагався створити безпечніший метод безпечного вводу / виводу.


2
Зрозуміло, що це небезпечно, і там має бути краща перевірка помилок компілятора, але без цього деякі корисні конструкції не обійтися, наприклад, оператор [] () у std :: map.
j_random_hacker

2
Повернення посилань, які не містять контрасту, насправді неймовірно корисні. vector::operator[]наприклад. Ви б краще написали v.setAt(i, x)чи v[i] = x? Останнє є FAR вище.
Майлз Рут

1
@MilesRout Я б пішов на v.setAt(i, x)будь-який час. Це FAR вище.
scravy

-2

Я зіткнувся з реальною проблемою там, де це було справді зло. По суті, розробник повернув посилання на об'єкт у векторі. Це було погано !!!

Повні деталі, про які я писав у «Янурарі»: http://developer-resource.blogspot.com/2009/01/pros-and-cons-of-returing-references.html


2
Якщо вам потрібно змінити вихідне значення в коді виклику, вам потрібно повернути посилання. І це насправді не більше і не менш небезпечно, ніж повернення ітератора до вектора - обидва недійсні, якщо елементи додані до вектора або видалені з них.
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
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.