У меня есть пара несвязанных классов, работающих вместе с помощью прослушивателей. Они оба держать копию своего соответствующего виртуального интерфейса, упрощенной, как это:На пороге программы удаления UB-сервера/клиентской памяти
struct Base
{
struct IFormat
{
virtual int formatVal(string &) = 0;
};
void addFmt(IFormat * fmt)
{
if (!fmts.contains(fmt))
fmts.push_back(fmt);
}
void removeFmt(IFormat * fmt)
{
auto it = fmts.find(fmt);
if (it != fmts.end())
fmts.erase(it);
}
int getValueFromFmts(string & input)
{
// this is getting called once in a while
int ret(0);
for (auto fmt : fmts)
ret += fmt->formatVal(input);
}
virtual ~Base()
{
// what now? something probably has a reference to me.
}
private:
vector<IFormat *> fmts;
};
struct Editor : public Base::IFormat
{
Editor(Base * base)
: base(base)
{
base->addFmt(this);
}
~Editor()
{
// base might be deleted!
base->removeFmt(this);
}
virtual int formatVal(string &) override { ... }
private:
Base * base;
};
Проблема, как указано в коде: Любой класс может быть удален в любой момент. Редактор может отменить регистрацию его форматирования, но если базовый класс сначала исчезнет, все становится плохо. Это проблема, в которой у меня много мест (это GUI-код), поэтому мне понадобилось общее решение. Я понял, что все «базовые» классы могут быть получены из общего уведомления об исключениях (сервер) и реализовать механизм уведомления в качестве клиентов этого сервера. Я создал этот кусок кода, который работает великолепно:
template<typename Derived>
class DestructionServer
{
public:
typedef Derived type;
class ObjectProxy
{
public:
ObjectProxy(const Derived * serverToPresent)
: server(serverToPresent)
{}
bool operator == (const Derived * other) const
{
return server == other;
}
bool operator != (const Derived * other) const
{
return server != other;
}
private:
const Derived * server;
};
class Client
{
friend class DestructionServer<Derived>;
public:
Client()
: server(nullptr)
{
}
typedef DestructionServer Server;
virtual ~Client()
{
if (server)
server->removeClientDestructor(this);
}
virtual void onObjectDestruction(const ObjectProxy & destroyedObject) = 0;
private:
void onDestruction(const Derived * derivedServer)
{
if (!server)
throw std::runtime_error("Fatal error: DestructionServer::Client has no server!");
// derivedServer should be able to downcast to server, without conversion
if (derivedServer != server)
throw std::runtime_error("Fatal error: derivedServer does not derive from server!");
// forget reference to server
server = nullptr;
// return an unmodifiable reference to the server
onObjectDestruction(derivedServer);
}
Server * server;
};
void removeClientDestructor(Client * client)
{
auto it = std::find(clients.begin(), clients.end(), client);
if (it != clients.end())
{
clients.erase(it);
}
}
void addClientDestructor(Client * client)
{
if (client && !std::contains(clients, client))
{
clients.push_back(client);
// this only happens if a client is loaded into multiple servers
// or the client is trying to add itself multiple times to the same server.
if (client->server)
throw std::runtime_error("Fatal error: Client already has a server!");
client->server = this;
}
}
virtual ~DestructionServer()
{
for (Client * client : clients)
{
// would like to use a dynamic_cast here to check the upcast,
// but it is not possible since
// ((Derived*)this) is actually deconstructed at this point...
// or can static_cast handle this?
// in effect this is UB, but it 'works'
if (const Derived * derivedServer = static_cast<const Derived *>(this))
{
client->onDestruction(derivedServer);
}
else
{
// in fact, the typeid() shouldn't work as well, here?
throw std::runtime_error(
std::string("Fatal error: ") + typeid(this).name() +
" doesn't derive from " + typeid(DestructionServer<Derived> *).name()
);
}
}
}
protected:
// make it impossible to construct this class without
// deriving from this class.
DestructionServer() {};
private:
std::vector<Client *> clients;
};
Теперь я могу переписать пример, как это, и держать его полностью удалить безопасным:
struct Base : public DestructionServer<Base>
{
struct IFormat : public Client
{
virtual int formatVal(string &) = 0;
};
void addFmt(IFormat * fmt)
{
if (!fmts.contains(fmt))
{
addClientDestructor(fmt);
fmts.push_back(fmt);
}
}
void removeFmt(IFormat * fmt)
{
auto it = fmts.find(fmt);
if (it != fmts.end())
{
fmts.erase(it);
}
}
int getValueFromFmts(string & input)
{
// this is getting called once in a while
int ret(0);
for (auto fmt : fmts)
ret += fmt->formatVal(input);
}
virtual ~Base()
{
// DestructionServer automatically notifies all
// objects that set a formatter to this class.
}
private:
vector<IFormat *> fmts;
};
struct Editor : public Base::IFormat
{
Editor(Base * base)
: base(base)
{
base->addFmt(this);
}
~Editor()
{
// base is now null if it is deleted
if(base)
base->removeFmt(this);
}
virtual int formatVal(string &) override { ... }
virtual void onObjectDestruction(const Base::ObjectProxy & object)
{
if (object == base)
{
// ok, base is destructed now. set it to null and go kill our self
base = nullptr;
delete this;
}
}
private:
Base * base;
};
Проблема
Если вы сделали это через стену кода и прочитали комментарии - спасибо, вы, вероятно, заметили, что я отбрасываю этот указатель из базового деконструктора в производный тип и вызывая уведомления. Зная полностью, что указатель, скорее всего, мусор (наполовину деконструированный), я разработал класс ObjectProxy, чтобы клиенты не использовали этот объект (хотя они все еще могут, если у них есть копия, очевидно).
Единственное, что позволяет ObjectProxy, - сравнить значение указателя на другое, того же типа (чтобы клиенты могли проверить, какой объект был разрушен), поэтому я не уверен, что это фактически неопределенное поведение - это никогда не разыскивает указатель.
Кроме того, может ли upcast когда-либо терпеть неудачу во время выполнения - мне нужен динамический_cast? Я почти уверен, что static_cast запрещает предоставление несвязанных типов шаблонов DestructionServer, поэтому неудачные типы будут терпеть неудачу во время компиляции.
Любые другие идеи для создания этой работы системы также приветствуется ...
Вы, вероятно, хотите 'std :: set' вместо' std :: vector '. O (log N). –
MSalters
Да, возможно. Тем не менее оптимизация - это наименьшее из моих проблем. – Shaggi