Uma pergunta sobre ponteiros inteligentes e sua Indeterminismo Inevitable
-
03-07-2019 - |
Pergunta
Eu tenho sido extensivamente usando ponteiros inteligentes (boost :: shared_ptr para ser exato) em meus projetos nos últimos dois anos. I compreender e apreciar os seus benefícios e eu geralmente gosto muito deles. Mas quanto mais eu usá-los, mais eu perca o comportamento determinista do C ++ com relação ao gerenciamento de memória e RAII que parece que eu gosto em uma linguagem de programação. ponteiros inteligentes simplificar o processo de gerenciamento de memória e fornecer coleta de lixo automática entre outras coisas, mas o problema é que o uso de coleta de lixo automática no ponteiro geral e inteligente introduz especificamente algum grau de indeterminisim na ordem de (de) inicializações. Este indeterminismo toma o controle longe dos programadores e, como eu vim a perceber ultimamente, faz o trabalho de concepção e APIs em desenvolvimento, o uso de que não é completamente conhecido antecipadamente no momento do desenvolvimento, irritantemente morosa todos os padrões de uso e casos de canto deve ser bem pensado.
Para elaborar mais, estou actualmente a desenvolver uma API. Partes deste API requer certos objetos a serem inicializados antes ou destruídas após outros objetos. Dito de outra forma, a ordem de inicialização (de) é importante, às vezes. Para lhe dar um exemplo simples, digamos que temos uma classe chamada do sistema. Um sistema fornece algumas funcionalidades básicas (logging no nosso exemplo) e possui uma série de subsistemas via ponteiros inteligentes.
class System {
public:
boost::shared_ptr< Subsystem > GetSubsystem( unsigned int index ) {
assert( index < mSubsystems.size() );
return mSubsystems[ index ];
}
void LogMessage( const std::string& message ) {
std::cout << message << std::endl;
}
private:
typedef std::vector< boost::shared_ptr< Subsystem > > SubsystemList;
SubsystemList mSubsystems;
};
class Subsystem {
public:
Subsystem( System* pParentSystem )
: mpParentSystem( pParentSystem ) {
}
~Subsystem() {
pParentSubsystem->LogMessage( "Destroying..." );
// Destroy this subsystem: deallocate memory, release resource, etc.
}
/*
Other stuff here
*/
private:
System * pParentSystem; // raw pointer to avoid cycles - can also use weak_ptrs
};
Como você já pode dizer, um subsistema só faz sentido no contexto de um sistema. Mas um subsistema de tal projeto pode facilmente sobreviver a sua controladora do sistema.
int main() {
{
boost::shared_ptr< Subsystem > pSomeSubsystem;
{
boost::shared_ptr< System > pSystem( new System );
pSomeSubsystem = pSystem->GetSubsystem( /* some index */ );
} // Our System would go out of scope and be destroyed here, but the Subsystem that pSomeSubsystem points to will not be destroyed.
} // pSomeSubsystem would go out of scope here but wait a second, how are we going to log messages in Subsystem's destructor?! Its parent System is destroyed after all. BOOM!
return 0;
}
Se tivéssemos usado ponteiros crus para subsistemas de espera, que teria destruído subsistemas quando o nosso sistema tinha ido para baixo, é claro, em seguida, pSomeSubsystem seria um apontador pendente.
Embora, não é o trabalho de um designer de API para proteger os programadores cliente de si mesmos, é uma boa idéia para fazer a API fácil de usar corretamente e difícil de usar incorretamente. Então eu estou pedindo a vocês. O que você acha? Como devo aliviar este problema? Como você projetar tal sistema?
Agradecemos antecipadamente, Josh
Solução
Resumo do problema
Existem duas preocupações concorrentes nesta questão.
- gestão do ciclo de vida de
Subsystem
s, permitindo sua remoção no momento certo. - Os clientes de
Subsystem
s precisa saber que oSubsystem
eles estão usando é válido.
Manipulação # 1
System
detém os Subsystem
s e deve gerir o seu ciclo de vida com seu próprio escopo. Usando shared_ptr
s para isso é particularmente útil uma vez que simplifica destruição, mas você não deve ser entregá-los, porque então você solta o determinismo que você está procurando com relação à sua desalocação.
Manipulação nº 2
Esta é a preocupação mais interessante para endereço. Descrevendo o problema com mais detalhes, você precisa de clientes para receber um objeto que se comporta como um Subsystem
enquanto que Subsystem
(e é System
pai) existe, mas se comporta de forma adequada após uma Subsystem
é destruído.
Este é facilmente resolvido por uma combinação da Proxy Padrão , o href="http://en.wikipedia.org/wiki/State_pattern" rel="nofollow noreferrer" title="Wikipedia: Estado padrão"> Estado Padrão e Null Objecto Padrão . Embora isso possa parecer ser um complexo de uma solução, pouco ' Há uma simplicidade única a ser tido no outro lado da complexidade .' Como desenvolvedores Biblioteca / API, é preciso ir a milha extra para tornar os nossos sistemas robustos. Além disso, queremos que os nossos sistemas a se comportar de forma intuitiva como um usuário espera, e à decadência graciosamente quando tentam abusar deles. Existem muitas soluções para este problema, no entanto, isso deve-se levá-lo a que todo ponto importante onde, como você e Scott Meyers dizer, é " fácil de usar corretamente e difícil de usar incorretamente '
Agora, estou assumindo que, na realidade, trata System
em alguma classe base do Subsystem
s, a partir do qual derivam vários Subsystem
s diferentes. Eu introduzi-lo abaixo como SubsystemBase
. Você precisa introduzir um Proxy objeto, SubsystemProxy
abaixo, que implementa a interface de SubsystemBase
encaminhando solicitações para o objeto que é proxy. (Neste sentido, é muito parecido com uma aplicação de propósito específico do Decorator Padrão). Cada Subsystem
cria um desses objetos, que detém através de um shared_ptr
, e retorna quando solicitado via GetProxy()
, que é chamado pelo objeto System
pai quando GetSubsystem()
é chamado.
Quando um System
sai do escopo, cada um de seus objetos Subsystem
fica destruído. Em seu destructor, que eles chamam mProxy->Nullify()
, o que provoca a sua Proxy objetos para mudar sua Estado . Eles fazem isso alterando a apontar para um Null objeto , que implementa a interface SubsystemBase
, mas fá-lo por não fazer nada.
Usando o Pattern Estado aqui tem permitido a aplicação do cliente para ser completamente alheio à existência ou não de um Subsystem
particular. Além disso, ele não precisa verificar ponteiros ou manter em torno casos que deveriam ter sido destruídas.
O Padrão Proxy permite que o cliente para ser dependente de um objeto leve quecompletamente embrulha-se os detalhes do funcionamento interno da API, e mantém uma constante, interface uniforme.
O Null Objecto Padrão permite que o Proxy para a função após a Subsystem
original tenha sido removida.
Exemplo de código
Eu tinha colocado uma pseudo-código de exemplo de qualidade áspera aqui, mas eu não estava satisfeito com ele. Eu reescrito-lo para ser um preciso, a compilação (eu usei g ++) exemplo do que eu descrevi acima. Para obtê-lo para o trabalho, eu tive que introduzir algumas outras classes, mas seus usos devem ser claras de seus nomes. I empregou o Singleton Pattern para a classe NullSubsystem
, como faz sentido que você não precisa mais de um. ProxyableSubsystemBase
abstrai completamente o comportamento Proxying longe do Subsystem
, permitindo que ele seja ignorante deste comportamento. Aqui está a UML Diagrama das classes:
Exemplo de código:
#include <iostream>
#include <string>
#include <vector>
#include <boost/shared_ptr.hpp>
// Forward Declarations to allow friending
class System;
class ProxyableSubsystemBase;
// Base defining the interface for Subsystems
class SubsystemBase
{
public:
// pure virtual functions
virtual void DoSomething(void) = 0;
virtual int GetSize(void) = 0;
virtual ~SubsystemBase() {} // virtual destructor for base class
};
// Null Object Pattern: an object which implements the interface to do nothing.
class NullSubsystem : public SubsystemBase
{
public:
// implements pure virtual functions from SubsystemBase to do nothing.
void DoSomething(void) { }
int GetSize(void) { return -1; }
// Singleton Pattern: We only ever need one NullSubsystem, so we'll enforce that
static NullSubsystem *instance()
{
static NullSubsystem singletonInstance;
return &singletonInstance;
}
private:
NullSubsystem() {} // private constructor to inforce Singleton Pattern
};
// Proxy Pattern: An object that takes the place of another to provide better
// control over the uses of that object
class SubsystemProxy : public SubsystemBase
{
friend class ProxyableSubsystemBase;
public:
SubsystemProxy(SubsystemBase *ProxiedSubsystem)
: mProxied(ProxiedSubsystem)
{
}
// implements pure virtual functions from SubsystemBase to forward to mProxied
void DoSomething(void) { mProxied->DoSomething(); }
int GetSize(void) { return mProxied->GetSize(); }
protected:
// State Pattern: the initial state of the SubsystemProxy is to point to a
// valid SubsytemBase, which is passed into the constructor. Calling Nullify()
// causes a change in the internal state to point to a NullSubsystem, which allows
// the proxy to still perform correctly, despite the Subsystem going out of scope.
void Nullify()
{
mProxied=NullSubsystem::instance();
}
private:
SubsystemBase *mProxied;
};
// A Base for real Subsystems to add the Proxying behavior
class ProxyableSubsystemBase : public SubsystemBase
{
friend class System; // Allow system to call our GetProxy() method.
public:
ProxyableSubsystemBase()
: mProxy(new SubsystemProxy(this)) // create our proxy object
{
}
~ProxyableSubsystemBase()
{
mProxy->Nullify(); // inform our proxy object we are going away
}
protected:
boost::shared_ptr<SubsystemProxy> GetProxy() { return mProxy; }
private:
boost::shared_ptr<SubsystemProxy> mProxy;
};
// the managing system
class System
{
public:
typedef boost::shared_ptr< SubsystemProxy > SubsystemHandle;
typedef boost::shared_ptr< ProxyableSubsystemBase > SubsystemPtr;
SubsystemHandle GetSubsystem( unsigned int index )
{
assert( index < mSubsystems.size() );
return mSubsystems[ index ]->GetProxy();
}
void LogMessage( const std::string& message )
{
std::cout << " <System>: " << message << std::endl;
}
int AddSubsystem( ProxyableSubsystemBase *pSubsystem )
{
LogMessage("Adding Subsystem:");
mSubsystems.push_back(SubsystemPtr(pSubsystem));
return mSubsystems.size()-1;
}
System()
{
LogMessage("System is constructing.");
}
~System()
{
LogMessage("System is going out of scope.");
}
private:
// have to hold base pointers
typedef std::vector< boost::shared_ptr<ProxyableSubsystemBase> > SubsystemList;
SubsystemList mSubsystems;
};
// the actual Subsystem
class Subsystem : public ProxyableSubsystemBase
{
public:
Subsystem( System* pParentSystem, const std::string ID )
: mParentSystem( pParentSystem )
, mID(ID)
{
mParentSystem->LogMessage( "Creating... "+mID );
}
~Subsystem()
{
mParentSystem->LogMessage( "Destroying... "+mID );
}
// implements pure virtual functions from SubsystemBase
void DoSomething(void) { mParentSystem->LogMessage( mID + " is DoingSomething (tm)."); }
int GetSize(void) { return sizeof(Subsystem); }
private:
System * mParentSystem; // raw pointer to avoid cycles - can also use weak_ptrs
std::string mID;
};
//////////////////////////////////////////////////////////////////
// Actual Use Example
int main(int argc, char* argv[])
{
std::cout << "main(): Creating Handles H1 and H2 for Subsystems. " << std::endl;
System::SubsystemHandle H1;
System::SubsystemHandle H2;
std::cout << "-------------------------------------------" << std::endl;
{
std::cout << " main(): Begin scope for System." << std::endl;
System mySystem;
int FrankIndex = mySystem.AddSubsystem(new Subsystem(&mySystem, "Frank"));
int ErnestIndex = mySystem.AddSubsystem(new Subsystem(&mySystem, "Ernest"));
std::cout << " main(): Assigning Subsystems to H1 and H2." << std::endl;
H1=mySystem.GetSubsystem(FrankIndex);
H2=mySystem.GetSubsystem(ErnestIndex);
std::cout << " main(): Doing something on H1 and H2." << std::endl;
H1->DoSomething();
H2->DoSomething();
std::cout << " main(): Leaving scope for System." << std::endl;
}
std::cout << "-------------------------------------------" << std::endl;
std::cout << "main(): Doing something on H1 and H2. (outside System Scope.) " << std::endl;
H1->DoSomething();
H2->DoSomething();
std::cout << "main(): No errors from using handles to out of scope Subsystems because of Proxy to Null Object." << std::endl;
return 0;
}
A saída do código:
main(): Creating Handles H1 and H2 for Subsystems.
-------------------------------------------
main(): Begin scope for System.
<System>: System is constructing.
<System>: Creating... Frank
<System>: Adding Subsystem:
<System>: Creating... Ernest
<System>: Adding Subsystem:
main(): Assigning Subsystems to H1 and H2.
main(): Doing something on H1 and H2.
<System>: Frank is DoingSomething (tm).
<System>: Ernest is DoingSomething (tm).
main(): Leaving scope for System.
<System>: System is going out of scope.
<System>: Destroying... Frank
<System>: Destroying... Ernest
-------------------------------------------
main(): Doing something on H1 and H2. (outside System Scope.)
main(): No errors from using handles to out of scope Subsystems because of Proxy to Null Object.
Other Thoughts:
-
Um artigo interessante que li em um dos Gems Jogo de programação livros fala sobre usando Null objetos para depuração e desenvolvimento. Eles estavam falando especificamente sobre o uso de modelos e texturas Null gráficos, como uma textura xadrez para fazer modelos faltando realmente se destaca. O mesmo poderia ser aplicado aqui, alterando o
NullSubsystem
para umaReportingSubsystem
que registrar a chamada e, possivelmente, a pilha de chamadas sempre que for acessado. Isso permitiria que você ou clientes da sua biblioteca para rastrear onde eles estão dependendo de algo que tem ido para fora do escopo, mas sem a necessidade de causar um acidente. -
I mencionado em um comentário @Arkadiy que a dependência circular fez subir entre
System
eSubsystem
é um pouco desagradável. Ele pode ser facilmente sanado por terSystem
derivar de uma interface em queSubsystem
depende, uma aplicação de Robert C Martin inversão de dependência Princípio . Melhor ainda seria isolar a funcionalidade queSubsystem
s precisa de seu pai, escrever uma interface para que, em seguida, agarrar um implementador de que a interface emSystem
e passá-lo para osSubsystem
s, que iria segurá-la através de umshared_ptr
. Por exemplo, você pode terLoggerInterface
, que seus usosSubsystem
para escrever para o log, então você poderia derivarCoutLogger
ouFileLogger
a partir dele, e manter uma instância de tal noSystem
.
Outras dicas
Este é capaz de fazer com o uso adequado da classe weak_ptr
. Na verdade, você já são bastante perto de ter uma boa solução. Está certo que você não pode esperar que "out-pensar" seus programadores do cliente, nem que você deve esperar que eles vão sempre seguir as "regras" da sua API (como eu tenho certeza que você já está ciente). Então, o melhor que você pode realmente fazer é controlar os danos.
Eu recomendo ter sua chamada para GetSubsystem
retornar um weak_ptr
em vez de um shared_ptr
simplesmente para que o desenvolvedor cliente pode testar a validade do ponteiro sem sempre reivindicando uma referência a ele.
Da mesma forma, tem pParentSystem
ser um boost::weak_ptr<System>
para que ele possa detectar internamente se o seu System
pai ainda existe através de uma chamada para lock
em pParentSystem
juntamente com um cheque de NULL
(a ponteiro bruto não irá dizer-lhe isso).
Assumindo que você mudar sua classe Subsystem
sempre verificar se há ou não a sua correspondente objeto System
existe, você pode garantir que, se as tentativas programador cliente para usar o exterior Subsystem
objeto do escopo planejado que ocorrerá um erro (que você controla) , ao invés de uma exceção inexplicável (que você deve confiar o programador cliente para catch / tratar adequadamente).
Assim, no seu exemplo com main()
, as coisas não vão BOOM! A maneira mais graciosa para lidar com isso em dtor do Subsystem
seria tê-lo parecido com este:
class Subsystem
{
...
~Subsystem() {
boost::shared_ptr<System> my_system(pParentSystem.lock());
if (NULL != my_system.get()) { // only works if pParentSystem refers to a valid System object
// now you are guaranteed this will work, since a reference is held to the System object
my_system->LogMessage( "Destroying..." );
}
// Destroy this subsystem: deallocate memory, release resource, etc.
// when my_system goes out of scope, this may cause the associated System object to be destroyed as well (if it holds the last reference)
}
...
};
Espero que isso ajude!
Aqui Sistema possui claramente os subsistemas e não vejo nenhum ponto em ter propriedade compartilhada. Eu simplesmente retornar um ponteiro bruto. Se um subsistema sobrevive seu Sistema, isso é um erro por conta própria.
Você estava certo no início, no seu primeiro parágrafo. Seus projetos com base em RAII (como o meu e mais bem escrito código C ++) exigem que os objetos são mantidos por ponteiros propriedade exclusiva. Em impulso que seria scoped_ptr.
Então, por que não usar scoped_ptr. Será, certamente, porque você queria os benefícios de weak_ptr para proteger contra referências pendurados, mas você só pode apontar um weak_ptr em um shared_ptr. Então você adotaram a prática comum de expediente declarando shared_ptr quando o que você realmente queria era única propriedade. Esta é uma falsa declaração e como você diz, que compromete destruidores sendo chamados na seqüência correta. Claro, se você nunca compartilhar a propriedade que você vai fugir com ele -. Mas você terá que verificar constantemente todo o seu código para se certificar de que nunca foi compartilhada
Para piorar o boost :: weak_ptr é inconveniente para uso (não tem operador ->) para que os programadores evitar este inconveniente por falsamente declarando passivo observando referências como shared_ptr. Este de ações curso propriedade e se você esquecer de nulo que shared_ptr em seguida, o objeto não são destruídas ou seu destruidor chamado quando você pretende que ele.
Em suma, você tem sido shafted pela biblioteca de impulso - ele não consegue abraçar bom C ++ programação práticas e programadores forças para fazer declarações falsas, a fim de tentar obter algum benefício a partir dele. Só é útil para o código cola scripting que realmente quer propriedade compartilhada e não está interessado em um controlo apertado sobre memória ou destruidores sendo chamado na seqüência correta.
Eu tenho sido o mesmo caminho que você. Proteção contra ponteiros pendurados é extremamente necessária em C ++, mas a biblioteca de impulso não fornece uma solução aceitável. Eu tinha que resolver este problema - o meu departamento software queria garantias de que C ++ pode ser feita segura. Então eu rolou meu próprio - foi bastante um monte de trabalho e pode ser encontrada em:
http://www.codeproject.com/KB/cpp/XONOR.aspx
É totalmente adequado para o trabalho de rosca única e estou prestes a atualizá-lo para abraçar ponteiros sendo compartilhados entre threads. Sua principal característica é que ele suporta inteligentes (self-zeroing) observadores passivos de objetos detidas.
Infelizmente programadores tornaram-se seduzido pela coleta de lixo e 'tamanho único' soluções de ponteiro inteligente e em grande parte não são sequer pensar sobre propriedade e observadores passivos - como resultado, eles nem sequer sabem que o que eles estão fazendo é errado e não se queixam. Heresia contra Boost é quase inédito!
As soluções que foram sugeridas para você são absurdamente complicada e de nenhuma ajuda em tudo. Eles são exemplos do absurdo que os resultados de uma relutância cultural a reconhecer que ponteiros de objeto têm papéis distintos que devem ser corretamente declarados e uma fé cega que o impulso deve ser a solução.
Eu não vejo um problema com ter System :: GetSubsystem retornar um ponteiro bruto (em vez de um ponteiro inteligente) para um subsistema. Desde que o cliente não é responsável pela construção dos objetos, então não há nenhum contrato implícito para o cliente para ser responsável pela limpeza. E já que é uma referência interna, que deve ser razoável supor que a vida útil do objeto Subsystem é dependente do tempo de vida do objeto do sistema. Você deve, então, reforçar este contrato implícito com a documentação afirmando tanto.
O ponto é, que você não está a reatribuição ou compartilhar propriedade -? Então, por que usar um ponteiro inteligente
O verdadeiro problema aqui é o seu design. Não há nenhuma boa solução, porque o modelo não reflete bons princípios de design. Aqui está uma regra útil de uso polegar I:
- Se um objeto contém uma coleção de outros objetos, e pode retornar qualquer objeto arbitrário dessa coleção, então remover o objeto de seu projeto .
Eu percebo que o seu exemplo é planejado, mas é uma anti-padrão que eu vejo um monte de trabalho. Pergunte a si mesmo, o valor é System
acrescentando que does not std::vector< shared_ptr<SubSystem> >
? Os usuários de sua necessidade API para conhecer a interface de SubSystem
(desde que você devolvê-los), assim que escrever um suporte para eles é apenas aumentar a complexidade. Pelo menos as pessoas sabem a interface para std::vector
, forçando-os a lembrar GetSubsystem()
acima at()
ou operator[]
é apenas média .
A sua pergunta é sobre o gerenciamento de vida dos objetos, mas uma vez que você começar a distribuir objetos, controle que você quer perder da vida, permitindo que outros para mantê-los vivos (shared_ptr
) ou o risco de falha se eles são usados ??depois que eles foram embora (raw ponteiros). Em multi-threaded aplicações sua pior ainda - que bloqueia os objetos que estão distribuindo a diferentes tópicos? Aumenta compartilhados e ponteiros fracos são uma complexidade induzir armadilha quando usado desta forma, especialmente porque eles estão apenas enfiar o suficiente seguro para viagem até desenvolvedores inexperientes.
Se você estiver indo para criar um suporte, ele precisa esconder a complexidade de seus usuários e isentá-las dos encargos você pode gerenciar a si mesmo. Como um exemplo, uma interface que consiste em a) comando de envio para o subsistema (por exemplo, um URI -? / Sistema / subsistema / comando param = valor) e b) subsistemas ITERATE e comandos de subsistema (por meio de um stl-like iteração) e, eventualmente, c) registar subsistema lhe permitiria esconder quase todos os detalhes da sua implementação a partir de seus usuários, e fazer cumprir o tempo de vida / ordenação / requisitos de bloqueio internamente.
Uma API iteratable / enumeráveis ??é vastamente preferível expor objetos em qualquer caso - os comandos / inscrições podem ser facilmente serializados para a geração de casos de teste ou arquivos de configuração, e eles poderiam ser exibidos de forma interativa (digamos, em um controle de árvore, com diálogos composta por examinando as ações disponíveis / parâmetros). Você também seria proteger seus usuários de API de mudanças internas que você pode precisar fazer às classes subsistema.
Eu adverti-lo contra a seguir os conselhos em resposta Aarons. Projetando uma solução para um problema tão simples que requer 5 padrões de design diferentes para implementar só pode significar que o problema errado está sendo resolvido. Eu também estou cansado de quem cita o Sr. Myers em relação ao design, uma vez por sua própria admissão:
"Eu não tenho escrito software de produção em mais de 20 anos, e eu nunca escrevi produção de software em C ++. Não, nem nunca. Além disso, eu nunca sequer tentou software de produção escrita em C ++, para que não só estou não um real C desenvolvedor ++, eu não sou mesmo um aspirante. Contrabalançando este é um pouco o fato de que eu fiz software de pesquisa de escrita em C ++ durante meus anos de escola de pós-graduação (1985-1993), mas mesmo isso foi pequena (alguns milhares de linhas) -desenvolvedor único para-ser-jogado-away-rapidamente coisas. E desde riscando como consultor sobre uma dúzia de anos atrás, a programação meu C ++ tem sido limitada a toy “vamos ver como isso funciona” (ou, às vezes, “vamos ver quantas compiladores isso quebra”) programas, tipicamente programas que se encaixam em um único arquivo".
Não quer dizer que seus livros não valem leitura, mas ele não tem autoridade para falar no design ou complexidade.
No seu exemplo, seria melhor se o sistema realizou uma vector<Subsystem>
em vez de um vector<shared_ptr<Subsystem> >
. Seus tanto mais simples e elimina a preocupação que você tem. GetSubsystem iria retornar uma referência em seu lugar.
objetos pilha será lançado na ordem oposta à qual onde instanciado, a menos que o desenvolvedor usando o API está a tentar gerir o ponteiro inteligente, que normalmente não vai ser um problema. Há apenas algumas coisas que você não vão ser capazes de prevenir, o melhor que você pode fazer é fornecer avisos em tempo de execução, de preferência depurar somente.
O seu exemplo parece muito COM gosta de mim, você tem contagem de referência sobre os subsistemas que estão sendo devolvidos usando shared_ptr, mas você está faltando-lo no próprio objeto do sistema.
Se cada um dos objetos subsistema fez um AddRef sobre o objeto do sistema na criação, e uma liberação de destruição você poderia pelo menos apresentar uma exceção se a contagem de referência foi incorreto quando o objeto do sistema é destruído cedo.
O uso de weak_ptr também permitirá que você forneça uma mensagem em vez / aswell como explodir quando as coisas são liberados na ordem errada.
A essência do problema é uma referência circular: Sistema refere-se a Subsystem, e Subsystem, por sua vez, refere-se ao sistema. Este tipo de estrutura de dados não podem ser facilmente manipulados por contagem de referência - que exige a coleta de lixo adequada. Você está tentando quebrar o loop usando um ponteiro bruto para uma das bordas -. Isso só vai produzir mais complicações
Pelo menos duas boas soluções foram sugeridas, por isso não vou tentar superar os cartazes anteriores. Eu só posso notar que, em @ solução de Aaron você pode ter um proxy para o sistema e não para Subsistemas -. Dependingo n o que é mais complexo e que faz sentido