Подтвердить что ты не робот

Обучение С++: возвращение ссылок и поиск

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

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

И результаты: "bark bark rawr rawr".

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

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

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

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

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

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

4b9b3361

Ответ 1

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

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

3) Ты не должен. Для этого нужны указатели.


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

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

Ответ 2

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

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

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

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

Псевдокод (на основе изобретенных слабых и общих указателей, я не использую 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.

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

Ответ 3

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

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

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

Animal& r2 = rFunc();

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

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

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

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

Ответ 4

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

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

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

Ответ 5

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

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

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

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

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

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

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

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

  • Как я должен возвращать ссылку на вновь созданный объект, не делая глупую вещь, которую я сделал выше в 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 в С++ создает объекты в памяти, где они не будут уничтожены, пока вы не скажете так. Но, чтобы уничтожить его, вы должны помнить, где он был создан в памяти. Вы делаете это, создавая переменную указателя, Animal *AnimalPointer;, и присваивая ему указатель, возвращаемый new Animal(), AnimalPointer = new Animal();. Чтобы уничтожить объект Animal, когда вы закончите с ним, вы должны ввести delete AnimalPointer;.

Ответ 6

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

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

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

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

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

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

Ответ 7

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

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

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

виртуальный bool Animal:: ест (Animal * other) = 0;

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

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

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

Ответ 8

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

Ответ 9

Я поместил интересный комментарий, и сообщение просто приняло "-1" Это не очень любезно. Не могли бы вы хотя бы объяснить, прежде чем отговаривать людей от такого поведения?

Ответ 10

Я искал объяснение по этому поводу. И после того, как я понял некоторые новые вещи, основанные на том, что я уже знал.

Доктор Скотт Мейерс, стр. 291 "Эффективного современного" C++, пишет о проблеме нарезки. Эта проблема решается путем правильного применения правил полиморфизма C++.

Я узнал, что существует 2 правила полиморфизма:

1) используя ключевое слово "виртуальный", как вы сделали. 2) Второй использует указатель или ссылку. Без этого второго правила вы столкнетесь с проблемой нарезки.

Ответ 11

Добрый день.

Доктор Скотт Мейерс, стр. 291 "Эффективного современного" C++, пишет о проблеме нарезки. Эта проблема решается путем правильного применения правил полиморфизма C++.

Без объявления указателя или ссылки вы получаете тип, который является базовым типом, даже если возвращаемый тип вашей функции использует указатель или ссылку. Важно иметь указатель или ссылку для объявления вашего объекта возврата.

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