Изучение C ++: возвращение ссылок и обход нарезки - PullRequest
10 голосов
/ 10 декабря 2010

У меня дьявол понимания времени.Рассмотрим следующий код:

class Animal
{
public:
    virtual void makeSound() {cout << "rawr" << endl;}
};

class Dog : public Animal
{
public:
    virtual void makeSound() {cout << "bark" << endl;}
};

Animal* pFunc()
{
    return new Dog();
}

Animal& rFunc()
{
    return *(new Dog());
}

Animal vFunc()
{
    return Dog();
}

int main()
{
    Animal* p = pFunc();
    p->makeSound();

    Animal& r1 = rFunc();
    r1.makeSound();

    Animal r2 = rFunc();
    r2.makeSound();

    Animal v = vFunc();
    v.makeSound();
}

И в результате получим: «кора кора сырой сырье».

С точки зрения Java (который, очевидно, испортил мою концептуализацию C ++), результатом будет «кора кора, кора кора».Из моего предыдущего вопроса я понимаю, что это различие связано с нарезкой, и теперь я хорошо понимаю, что такое нарезка.

Но давайте скажем, что мне нужна функция, которая возвращает значение Animal, которое действительно является Dog.

Правильно ли я понимаю, что самое близкое, что я могу получить - это ссылка ? Кроме того, обязан ли тот, кто использует интерфейс rFunc, видеть, что возвращаемая ссылка назначена Animal &?(Или иным образом намеренно присваивайте ссылку животному, которое посредством нарезки отбрасывает полиморфизм.) Как же я должен возвращать ссылку на вновь сгенерированный объект, не делая глупостей, которые я делал выше в rFunc?(По крайней мере, я слышал, что это глупо.)

Обновление: поскольку все, похоже, до сих пор согласны с тем, что rFunc нелегитимен, возникает другой связанный с этим вопрос:

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

Примечание: Все это приводит к идее шаблонной концепции shared_pimpl, над которой я работал.Надеюсь, я узнаю достаточно, чтобы опубликовать что-нибудь об этом через пару дней.

Ответы [ 8 ]

5 голосов
/ 10 декабря 2010

1) Если вы создаете новые объекты, вы никогда не захотите возвращать ссылку (см. Свой собственный комментарий к # 3.) Вы можете вернуть указатель (возможно, обернутый std::shared_ptr или std::auto_ptr).(Вы также можете вернуть копию, но это несовместимо с использованием оператора new; это также немного несовместимо с полиморфизмом.)

2) rFunc просто неправильно.Не делай этого.Если вы использовали new для создания объекта, то возвращайте его через (необязательно) указатель.

3) Вы не должны этого делать.Вот для чего нужны указатели.


РЕДАКТИРОВАТЬ (отвечая на ваше обновление :) Трудно представить сценарий, который вы описываете.Будет ли более точным сказать, что возвращенный указатель может быть недействительным, если вызывающий объект вызовет какой-то другой (определенный) метод?

Я бы посоветовал не использовать такую ​​модель, но если вам абсолютно необходимоэто, и вы должны применить это в своем API, тогда вам, вероятно, нужно добавить уровень косвенности, или даже два.Пример: обернуть реальный объект в объект с подсчетом ссылок, который содержит реальный указатель.Указатель объекта с подсчетом ссылок устанавливается на null, когда реальный объект удаляется.Это безобразно(Возможно, есть лучшие способы сделать это, но они все еще могут быть безобразными.)

2 голосов
/ 10 декабря 2010

Чтобы ответить на вторую часть вашего вопроса («как мне сообщить, что указатель подлежит удалению в любое время») -

Это опасная практика, и в ней есть тонкие детали, которые вам понадобятсярассматривать.По своей природе это неестественно.

Если указатель можно удалить в любой момент времени, использовать его из другого контекста небезопасно, потому что даже если вы проверите «Вы все еще действительны?»каждый раз, он может быть удален лишь чуть-чуть после проверки, но прежде чем вы сможете его использовать.

Безопасный способ сделать это - концепция «слабого указателя» - сохранить объект какразделяемый указатель (один уровень косвенности, может быть освобожден в любое время), и возвращаемое значение должно быть слабым указателем - то, что вы должны запрос , прежде чем вы можете использовать, и должны выпустить после того, как выиспользовал это.Таким образом, пока объект остается действительным, вы можете использовать его.

Псевдокод (на основе придуманных слабых и общих указателей, я не использую Boost ...) -

weak< Animal > animalWeak = getAnimalThatMayDisappear();
// ...
{
    shared< Animal > animal = animalWeak.getShared();
    if ( animal )
    {
        // 'animal' is still valid, use it.
        // ...
    }
    else
    {
        // 'animal' is not valid, can't use it. It points to NULL.
        // Now what?
    }
}
// And at this point the shared pointer of 'animal' is implicitly released.

Но это сложно и подвержено ошибкам, и, вероятно, сделает вашу жизнь сложнее.Я бы порекомендовал перейти на более простые проекты, если это возможно.

1 голос
/ 17 декабря 2010

Но давайте скажем, что мне нужна функция, которая возвращает значение Animal, которое действительно является Dog.

  1. Правильно ли я понимаю, что самое близкое, что я могу получить, - это ссылка?

Да, вы правы. Но я думаю, что проблема не в том, что вы не понимаете ссылки, а в том, что вы не понимаете различные типы переменных в C ++ или как new работает в C ++. В C ++ переменные могут быть примитивными данными (int, float, double и т. Д.), Объектом или указателем / ссылкой на примитив и / или объект. В Java переменные могут быть только примитивами или ссылками на объект.

В C ++, когда вы объявляете переменную, фактическая память выделяется и связывается с переменной. В Java вы должны явно создавать объекты, используя new, и явно назначать новый объект переменной. Ключевым моментом здесь является то, что в C ++ объект и переменная, которую вы используете для доступа, не одно и то же, когда переменная является указателем или ссылкой. Animal a; означает что-то отличное от Animal *a;, что означает что-то отличное от Animal &a;. Ни у одного из них нет совместимых типов, и они не являются взаимозаменяемыми.

Когда вы печатаете, Animal a1 в C ++. Новый объект Animal создан. Таким образом, когда вы набираете Animal a2 = a1;, вы получаете две переменные (a1 и a2) и два Animal объекта в разных местах памяти. Оба объекта имеют одинаковое значение, но вы можете изменить их значения независимо, если хотите. В Java, если вы ввели один и тот же точный код, вы бы получили две переменные, но только один объект. Если вы не переназначили ни одну из переменных, они всегда будут иметь одно и то же значение.

  1. Кроме того, обязан ли тот, кто использует интерфейс rFunc, видеть, что возвращаемая ссылка назначена Animal &? (Или иным образом намеренно назначьте ссылку на Животное, которое посредством нарезки отбрасывает полиморфизм.)

Когда вы используете ссылки и указатели, вы можете получить доступ к значению объекта, не копируя его туда, где вы хотите его использовать. Это позволяет вам изменить его вне фигурных скобок, где вы объявили объект в существование. Ссылки обычно используются в качестве параметров функции или для возврата закрытых членов данных объекта без создания их новой копии. Как правило, когда вы получаете ссылку, вы ничего не назначаете. Используя ваш пример, вместо присвоения переменной, возвращаемой rFunc(), переменной, обычно введите rFunc().makeSound();.

Так что, да, пользователь rFunc() обязан, если он присваивает возвращаемое значение чему-либо, назначить его для ссылки. Вы можете понять почему. Если вы назначите ссылку, возвращаемую rFunc(), переменной, объявленной как Animal animal_variable, вы получите одну Animal переменную, один Animal объект и один Dog объект. Объект Animal, связанный с animal_variable, является, насколько это возможно, копией объекта Dog, который был возвращен по ссылке из rFunc(). Но вы не можете получить полиморфное поведение от animal_variable, потому что эта переменная не связана с Dog объектом. Объект Dog, который был возвращен по ссылке, все еще существует, потому что вы создали его с помощью new, но он больше недоступен - утечка произошла.

  1. Как же я должен возвращать ссылку на вновь сгенерированный объект, не делая глупостей, которые я делал выше в rFunc? (По крайней мере, я слышал, что это глупо.)

Проблема в том, что вы можете создать объект тремя способами.

{ // the following expressions evaluate to ...  
 Animal local;  
 // an object that will be destroyed when control exits this block  
 Animal();  
 // an unamed object that will be destroyed immediately if not bound to a reference  
 new Animal();  
 // an unamed Animal *pointer* that can't be deleted unless it is assigned to a Animal pointer variable.  
 {  
  // doing other stuff
 }  
} // <- local destroyed

Все, что new делает в C ++, это создает объекты в памяти, где они не будут уничтожены, пока вы не скажете это. Но чтобы уничтожить его, вы должны помнить, где он был создан в памяти. Вы делаете это путем создания переменной указателя, Animal *AnimalPointer;, и присвоение указателя, возвращенного new Animal(), AnimalPointer = new Animal();. Чтобы уничтожить объект Animal, когда вы закончите с ним, вы должны набрать delete AnimalPointer;.

1 голос
/ 10 декабря 2010

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

Если вы действительно не можете доверять пользователю, тогда вообще не давайте ему указатель: передайте обратно дескриптор целочисленного типа и откройте интерфейс в стиле C (например, у вас есть вектор экземпляров на вашей сторонезабор, и вы выставляете функцию, которая принимает целое число в качестве первого параметра, индексирует вектор и вызывает функцию-член).Это старомодный способ (несмотря на то, что у нас не всегда были такие причудливые вещи, как «функции-члены»;)).

В противном случае попробуйте использовать умный указатель с соответствующей семантикой.Никто в здравом уме не подумает, что delete &*some_boost_shared_ptr; хорошая идея.

1 голос
/ 10 декабря 2010

Чтобы избежать нарезки, вы должны вернуть или передать указатель на объект. (Обратите внимание, что ссылка - это «постоянно разыменованный указатель».

Animal r2 = rFunc();
r2.makeSound();

Здесь создается экземпляр r2 (с использованием сгенерированной компилятором копии ctor), но он уходит Части собаки Если вы сделаете это так, нарезка не произойдет:

Animal& r2 = rFunc();

Однако ваша функция vFunc () разрезает внутри самого метода.

Я также упомяну эту функцию:

Animal& rFunc()
{
    return *(new Dog());
}

Это странно и небезопасно; вы создаете ссылку на временную безымянную переменную (разыменованный Dog). Более уместно вернуть указатель. Возвращаемые ссылки обычно используются для возврата переменных-членов и т. Д.

0 голосов
/ 18 апреля 2013

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

0 голосов
/ 10 декабря 2010

Пункт 1: не используйте ссылки.Используйте указатели.

Пункт 2: то, что у вас есть выше, называется таксономией, которая представляет собой схему иерархической классификации.Таксономии являются примером такого рода, который совершенно не подходит для объектно-ориентированного моделирования.Ваш тривиальный пример работает только потому, что ваше базовое животное предполагает, что все животные издают шум, и не может делать ничего более интересного.

Если вы попытаетесь реализовать отношение, такое как

virtual bool Animal :: eats (Animal * other) = 0;

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

Например: у позвоночных есть позвоночник, и мы можем спросить, сделан ли он из хряща или кости ... мы можемдаже не задавайте этот вопрос беспозвоночным.

Чтобы полностью понять, вы должны понять, что вы не можете сделать объект Dog.В конце концов, это абстракция, верно?Потому что есть келпи и колли, и отдельная собака должна быть какого-то вида ... Схема классификации может быть настолько глубокой, насколько вам нравится, но она никогда не сможет поддерживать каких-либо конкретных особей.Фидо не собака , это просто его классификационная метка.

0 голосов
/ 10 декабря 2010

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

Ваши проблемы расщепления исчезают, когда Animal является абстрактным базовым классом.Это означает, что он имеет по крайней мере один чисто виртуальный метод и не может быть непосредственно создан.Следующим становится ошибка компилятора:

Animal a = rFunc();   // a cannot be directly instantiated
                      // spliting prevented by compiler!

, но компилятор допускает:

Animal* a = pFunc();  // polymorphism maintained!
Animal& a = rFunc();  // polymorphism maintained!

Таким образом, компилятор спасает день!

...