C ++: как предотвратить разрушение объектов, построенных в аргументе? - PullRequest
2 голосов
/ 15 января 2010

У меня есть квесты вокруг такого персонала.

Существует класс A , у которого в качестве члена есть объект типа класса B . Поскольку я хотел бы, чтобы B был базовым классом группы других классов, мне нужно использовать указатель или ссылку на объект, а не на его копию, чтобы правильно использовать виртуальные методы B внутри A. Но когда я пишу такой код

class B
  {public:
     B(int _i = 1): i(_i) {};
     ~B() 
       {i = 0; // just to indicate existence of problem: here maybe something more dangerous, like delete [] operator, as well! 
        cout << "B destructed!\n";
       };
     virtual int GetI () const  {return i;}; // for example
   protected:
     int i;
  };

class A
  {public:
      A(const B& _b): b(_b) {}
      void ShowI () {cout <<b.GetI()<<'\n';};
   private:
      const B& b;
  }; 

и используйте его таким образом

B b(1);
A a(b);
a.ShowI();

отлично работает:

1
B destructed!

Но

A a(B(1));
a.ShowI();

дает очень нежелательный результат: объект b создает и a.b задается как ссылка на него, но сразу после завершения конструктора A объект b разрушается! Выход:

B destructed!
0

Еще раз повторяю, что вместо A используется копия вместо ссылки на b в A

class A
  {public:
      A(B _b): b(_b) {}
      void ShowI () {cout <<b.GetI()<<'\n';};
   private:
      B b;
  }; 

не будет работать, если B является базовым классом, а A вызывает его виртуальную функцию. Может быть, я слишком глуп, так как не знаю, не знаю, как правильно написать необходимый код, чтобы он работал идеально (тогда извините!), Или, может быть, это не так просто :-(

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

Ответы [ 7 ]

5 голосов
/ 15 января 2010

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

Решение совсем не сложно и не сложно. Полиморфный объект обычно не является временным, но он существует в течение более длительного времени. Таким образом, вы просто используете умный указатель для хранения ссылки на переданный объект и заранее создаете объект с помощью new. Обратите внимание, что для этого у класса B должен быть виртуальный деструктор. shared_ptr позаботится о том, чтобы уничтожить объект, когда срок действия вашего родительского объекта закончится. При копировании shared_ptr поделится указанным объектом с копией, и оба будут указывать на один и тот же объект. Это не обязательно очевидно для пользователей OnwsPolymorphics и не предназначено для многих случаев использования, поэтому вы можете захотеть сделать его конструктор копирования и оператор присваивания приватным:

struct OwnsPolymorphics {
  explicit OwnsPolymorphics(B *b):p(b) { }

private:
  // OwnsPolymorphics is not copyable. 
  OwnsPolymorphics(OwnsPolymorphics const&);
  OwnsPolymorphics &operator=(OwnsPolymorphics);

private:
  boost::shared_ptr<B> p;
};

OwnsPolymorphics owns(new DerivedFromB);

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


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

B const& b = DerivedFromB(); 

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

2 голосов
/ 15 января 2010

Когда вы пишете следующее:

A a(B(1));
a.ShowI();

объект B, переданный конструктору A, является временным объектом, который разрушается, как только конструктор A завершает работу (как вы заметили). Вы требуете, чтобы объект B жил так долго, как этого требует A (поскольку A использует ссылку на объект).

Вы можете использовать std :: auto_ptr (если вы хотите передать семантику владения, т. Е. Объект, которому принадлежит объект B, переданный его конструктору) или иным образом совместно используемый указатель (такой как Boost shared_ptr ).

2 голосов
/ 15 января 2010

Используйте какой-нибудь умный указатель вместо ссылки.

Я предлагаю Увеличить умные указатели .

1 голос
/ 15 января 2010

Это стандартная проблема, не бойтесь.

Во-первых, вы можете сохранить свой дизайн с небольшим изменением:

class A
{
public:
  A(B& b): m_b(b) {}
private:
  B& m_b;
};

Используя ссылку вместо константной ссылки, компилятор отклонит вызов конструктора А, сделанного с временным, потому что нельзя брать ссылку из временного.

Не существует (прямого) решения для фактического сохранения const, поскольку, к сожалению, компиляторы принимают странную конструкцию &B(), даже если это означает получение временного адреса (и они даже не стесняются сделать его указатель на неконстантный ...).

Существует ряд так называемых умных указателей. Базовый в STL называется std::auto_ptr. Другой (известный) - boost::shared_ptr.

Эти указатели называются умными, потому что они позволяют вам не беспокоиться (слишком много) об уничтожении объекта и фактически гарантируют вам, что он БУДЕТ уничтожен, и это правильно. Таким образом, вам никогда не придется беспокоиться о звонке delete.

Одно предостережение: не используйте std::auto_ptr. Это зверь, потому что у него неестественное поведение в отношении копирования.

std::auto_ptr<A> a(new A());  // Building
a->myMethod();                // Fine    

std::auto_ptr<A> b = a;       // Constructing b from a
b->myMethod();                // Fine
a->myMethod();                // ERROR (and usually crash)

Проблема в том, что копирование (с использованием конструкции копирования) или присвоение (с использованием оператора присваивания) означает передачу права собственности от скопированного на копирование ... ОЧЕНЬ СЮРПРИЗИН.

Если у вас есть доступ к готовящемуся стандарту, вы можете использовать std::unique_ptr, очень похоже на автоматический указатель, за исключением плохого поведения: его нельзя скопировать или назначить.

В то же время, вы можете просто использовать boost::shared_ptr или, возможно, std::tr1::shared_ptr. Они несколько идентичны.

Они являются прекрасным примером указателей с подсчетом ссылок. И они умны в этом.

std::vector< boost::shared_ptr<A> > method()
{
  boost::shared_ptr<A> a(new A());        // Create an `A` instance and a pointer to it
  std::vector< boost::shared_ptr<A> > v;
  v.push_back(a);                         // 2 references to the A instance
  v.push_back(a);                         // 3 references to the A instance
  return v;
}                                         // a is destroyed, only 2 references now

void function()
{
  std::vector< boost::shared_ptr<A> > w = method(); // 2 instances
  w.erase(w.begin());                               // remove w[0], 1 instance
}                                                   // w is destroyed, 0 instance
                                                    // upon dying, destroys A instance

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

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

boost::shared_ptr<A> a(new A());
boost::shared_ptr<A> b(new A(*a)); // copies *a into *b, b has its own instance

Итак, подведем итог:

  • не используйте auto_ptr, у вас будут неприятные сюрпризы
  • используйте unique_ptr, если доступно, это более безопасная ставка, и с ней легче всего иметь дело
  • используйте share_ptr в противном случае, но остерегайтесь семантики поверхностного копирования

Удачи!

1 голос
/ 15 января 2010

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

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

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

Если бы вы переключились на указатель, вы бы добавили вызов удаления в деструктор А, и ваша реализация выглядела бы так:

A a(new B(1));
0 голосов
/ 15 января 2010

Это понятное и правильное поведение. Я мог бы рекомендовать изменить распределение из стека в кучу. Умные указатели могут помочь вам избежать явного вызова деструктора.

std::auto_ptr<B> b(new B())
A a(*b); //using overriden operator * to get B& 
0 голосов
/ 15 января 2010

C ++ Стандарт 8.5.3 Пункт 5

Конструктор, который будет использоваться для создания копии, должен вызываться является ли копия на самом деле сделанный. [Пример:

struct A { };
struct B : public A { } b;
extern B f();
const A& rca = f();  // Either bound to the A sub-object of the B rvalue,
                     // or the entire B object is copied and the reference
                     //  is bound to the A sub-object of the copy
—end example]

Дело в том, что A a(B(1)); очень плохо. struct A получить ссылку на временный.

...