No C ++, o programador deve decidir como os recursos usados serão liberados; não há ferramentas automáticas como o coletor de lixo. O artigo discute possíveis soluções para esse problema, considera possíveis problemas em detalhes, além de vários problemas relacionados.
Sumário
1. Introdução
Gerenciamento de recursos é algo que um programador C ++ precisa fazer o tempo todo. Os recursos incluem blocos de memória, objetos do kernel do SO, bloqueios multithread, conexões de rede, conexões de banco de dados e qualquer objeto criado na memória dinâmica. O acesso ao recurso é por meio de um descritor, o tipo do descritor geralmente é um ponteiro ou um de seus aliases ( HANDLE
, etc.), às vezes o todo (descritores de arquivo UNIX). Depois de usar o recurso, você deve liberá-lo, caso contrário, mais cedo ou mais tarde, um aplicativo que não libera recursos (e possivelmente outros aplicativos) ficará sem recursos. Esse problema é muito grave, podemos dizer que um dos principais recursos do .NET, Java e várias outras plataformas é um sistema de gerenciamento de recursos unificado baseado na coleta de lixo.
Os recursos orientados a objeto do C ++ naturalmente levam à seguinte solução: a classe que gerencia o recurso contém o descritor de recursos como membro, inicializa o descritor quando o recurso é capturado e libera o recurso no destruidor. Mas depois de algum pensamento (ou experiência) vem a compreensão de que não é tão simples. E o principal problema é a semântica da cópia. Se a classe que gerencia o recurso usa o construtor de cópias gerado pelo compilador padrão, depois de copiar o objeto, obteremos duas cópias do identificador do mesmo recurso. Se um objeto libera um recurso, depois disso, o segundo poderá tentar usar ou liberar o recurso já liberado, o que, em qualquer caso, está incorreto e pode levar ao chamado comportamento indefinido, ou seja, qualquer coisa pode acontecer, por exemplo, uma interrupção anormal do programa.
Felizmente, em C ++, um programador pode controlar totalmente o processo de cópia, definindo um construtor de cópias e um operador de atribuição de cópias sozinho, o que nos permite resolver o problema acima, e geralmente não de uma maneira. A implementação da cópia deve estar intimamente ligada ao mecanismo de liberação do recurso, e chamaremos isso coletivamente de estratégia de propriedade da cópia. A chamada "regra das Três Grandes" é bem conhecida, que afirma que se um programador define pelo menos uma das três operações - construtor de cópias, operador de atribuição de cópias ou destruidor - ele deve definir as três operações. As estratégias de propriedade de cópia apenas especificam como fazer isso. Existem quatro estratégias básicas de propriedade de cópia.
1. Estratégias básicas de propriedade de cópias
Antes da captura do recurso ou após seu lançamento, o descritor deve assumir um valor especial indicando que ele não está associado ao recurso. Normalmente, este é zero, às vezes -1, convertido em um tipo de descritor. De qualquer forma, esse descritor será chamado zero. A classe que gerencia o recurso deve reconhecer o descritor nulo e não tentar usar ou liberar o recurso nesse caso.
1.1 Estratégia de proibição de cópia
Esta é a estratégia mais simples. Nesse caso, é simplesmente proibido copiar e atribuir instâncias de classe. O destruidor libera o recurso capturado. No C ++, proibir a cópia não é difícil, a classe deve declarar, mas não definir, o construtor de cópia fechada e o operador de atribuição de cópia.
class X { private: X(const X&); X& operator=(const X&);
As tentativas de cópia são frustradas pelo compilador e vinculador.
O padrão C ++ 11 oferece uma sintaxe especial para este caso:
class X { public: X(const X&) = delete; X& operator=(const X&) = delete;
Essa sintaxe é mais visual e fornece mensagens mais compreensíveis para o compilador ao tentar copiar.
Na versão anterior da biblioteca padrão (C ++ 98), as classes de fluxos de entrada / saída ( std::fstream
, etc.) usavam a estratégia de proibição de cópia e, no Windows, muitas classes do MFC ( CFile
, CEvent
, CMutex
etc.). Na biblioteca padrão do C ++ 11, algumas classes usam essa estratégia para oferecer suporte à sincronização multithread.
1.2 Estratégia de Propriedade Exclusiva
Nesse caso, ao implementar cópia e atribuição, o descritor de recursos passa do objeto de origem para o objeto de destino, ou seja, permanece em uma única cópia. Após copiar ou atribuir, o objeto de origem possui um descritor nulo e não pode usar o recurso. O destruidor libera o recurso capturado. Os termos propriedade exclusiva ou estrita [Josuttis] também são usados para esta estratégia; Andrei Alexandrescu usa o termo cópia destrutiva. No C ++ 11, isso é feito da seguinte maneira: a cópia regular e a atribuição de cópias são proibidas da maneira descrita acima, e a semântica de movimento é implementada, ou seja, o construtor de movimentação e o operador de atribuição de movimentação são definidos. (Mais sobre semântica de movimento posteriormente.)
class X { public: X(const X&) = delete; X& operator=(const X&) = delete; X(X&& src) noexcept; X& operator=(X&& src) noexcept;
Assim, a estratégia de propriedade exclusiva pode ser considerada uma extensão da estratégia de proibição de cópias.
Na biblioteca padrão do C ++ 11, essa estratégia usa o ponteiro inteligente std::unique_ptr<>
e algumas outras classes, por exemplo: std::thread
, std::unique_lock<>
, bem como as classes que usavam anteriormente a estratégia de proibição de cópia ( std::fstream
, etc.). No Windows, as classes MFC que usavam anteriormente a estratégia de proibição de cópia também começaram a usar a estratégia de propriedade exclusiva ( CFile
, CEvent
, CMutex
etc.).
1.3 Estratégia de cópia em profundidade
Nesse caso, você pode copiar e atribuir instâncias de classe. É necessário definir o construtor de cópia e o operador de atribuição de cópia, para que o objeto de destino copie o recurso para si do objeto de origem. Depois disso, cada objeto possui sua cópia do recurso, pode usar, modificar e liberar independentemente o recurso. O destruidor libera o recurso capturado. Às vezes, para objetos que usam a estratégia de cópia em profundidade, o termo objetos de valor é usado.
Essa estratégia não se aplica a todos os recursos. Ele pode ser aplicado a recursos associados a um buffer de memória, como strings, mas não está muito claro como aplicá-lo a objetos do kernel do sistema operacional, como arquivos, mutexes etc.
A estratégia de cópia em profundidade é usada em todos os tipos de cadeias de objetos, std::vector<>
e em outros contêineres da biblioteca padrão.
1.4 Estratégia de copropriedade
Nesse caso, você pode copiar e atribuir instâncias de classe. Você deve definir o construtor de cópia e o operador de atribuição de cópia no qual o descritor de recursos (assim como outros dados) é copiado, mas não o próprio recurso. Depois disso, cada objeto possui sua própria cópia do descritor, pode usar, modificar, mas não pode liberar o recurso, desde que haja pelo menos mais um objeto que possua uma cópia do descritor. Um recurso é liberado após o último objeto que possui uma cópia do identificador ficar fora do escopo. Como isso pode ser implementado está descrito abaixo.
As estratégias de copropriedade são frequentemente usadas por indicadores inteligentes, e também é natural usá-las para recursos imutáveis. O ponteiro inteligente std::shared_ptr<>
implementa essa estratégia na biblioteca padrão do C ++ 11.
2. Estratégia de cópia profunda - problemas e soluções
Considere um modelo para a função de troca de estado de objetos do tipo T
na biblioteca padrão do C ++ 98.
template<typename T> void swap(T& a, T& b) { T tmp(a); a = b; b = tmp; }
Se o tipo T
possui um recurso e usa uma estratégia de cópia profunda, temos três operações para alocar um novo recurso, três operações de cópia e três operações para liberar recursos. Embora na maioria dos casos, essa operação possa ser realizada sem alocar novos recursos e copiar, basta que os objetos troquem dados internos, incluindo um descritor de recursos. Existem muitos exemplos semelhantes quando você precisa criar cópias temporárias de um recurso e liberá-las imediatamente. Uma implementação tão ineficaz das operações cotidianas estimulou a busca de soluções para sua otimização. Vamos considerar as principais opções.
2.1 Copiar em registro
A cópia na gravação (COW), também chamada de cópia diferida, pode ser vista como uma tentativa de combinar uma estratégia de cópia profunda e uma estratégia de propriedade compartilhada. Inicialmente, ao copiar um objeto, o descritor de recurso é copiado, sem o próprio recurso, e para os proprietários, o recurso se torna compartilhado e somente leitura, mas assim que algum proprietário precisa modificar o recurso compartilhado, o recurso é copiado e, em seguida, esse proprietário trabalha com ele. uma cópia A implementação da COW resolve o problema da troca de estados: a alocação adicional de recursos e a cópia não ocorrem. O uso de COW é bastante popular na implementação de strings; por exemplo, CString
(MFC, ATL). Uma discussão sobre possíveis maneiras de implementar a COW e questões emergentes pode ser encontrada em [Meyers1], [Sutter]. [Guntheroth] propôs uma implementação de COW usando std::shared_ptr<>
. Existem problemas ao implementar o COW em um ambiente multiencadeado, e é por isso que é proibido usar o COW para seqüências de caracteres na biblioteca C ++ 11 padrão, consulte [Josuttis], [Guntheroth].
O desenvolvimento da ideia COW leva ao seguinte esquema de gerenciamento de recursos: o recurso é imutável e gerenciado por objetos usando a estratégia de propriedade compartilhada; se necessário, altere o recurso, um novo recurso modificado adequadamente será criado e um novo objeto proprietário será retornado. Esse esquema é usado para seqüências de caracteres e outros objetos imutáveis nas plataformas .NET e Java. Na programação funcional, é usado para estruturas de dados mais complexas.
2.2 Definindo uma Função de Troca de Estado para uma Classe
Foi demonstrado acima como a função de troca de estado pode ser ineficiente, implementada de maneira direta, por meio de cópia e atribuição. E é amplamente utilizado, por exemplo, é usado por muitos algoritmos da biblioteca padrão. Para que os algoritmos não usem outro std::swap()
, mas outra função definida especificamente para a classe, duas etapas devem ser executadas.
1. Defina na classe uma função membro Swap()
(o nome não é importante) que implementa a troca de estados.
class X { public: void Swap(X& other) noexcept;
Você deve garantir que essa função não noexcept
exceções; no C ++ 11, essas funções devem ser declaradas como não noexcept
.
2. No mesmo espaço para nome da classe X
(geralmente no mesmo arquivo de cabeçalho), defina a função swap()
livre (não membro) da seguinte maneira (o nome e a assinatura são fundamentais):
inline void swap(X& a, X& b) noexcept { a.Swap(b); }
Depois disso, os algoritmos da biblioteca padrão a usarão, não std::swap()
. Isso fornece um mecanismo chamado pesquisa dependente de argumento (ADL). Para mais informações sobre ADL, consulte [Dewhurst1].
Na biblioteca padrão C ++, todos os contêineres, ponteiros inteligentes e outras classes implementam a função de troca de estado conforme descrito acima.
A função de membro Swap()
geralmente é facilmente definida: é necessário aplicar seqüencialmente uma operação de troca de estado aos bancos de dados e membros, se eles suportarem, e std::swap()
caso contrário.
A descrição acima é um pouco simplificada, uma mais detalhada pode ser encontrada em [Meyers2]. Uma discussão sobre questões relacionadas à função de troca de estados também pode ser encontrada em [Sutter / Alexandrescu].
A função de troca de estado pode ser atribuída a uma das operações básicas da classe. Com ele, você pode definir outras operações com elegância. Por exemplo, o operador de atribuição de cópia é definido por meio de copy e Swap()
seguinte maneira:
X& X::operator=(const X& src) { X tmp(src); Swap(tmp); return *this; }
Esse modelo é chamado de idioma de cópia e troca ou idioma de Herb Sutter. Para mais detalhes, consulte [Sutter], [Sutter / Alexandrescu], [Meyers2]. Sua modificação pode ser aplicada para implementar a semântica do deslocamento, consulte as seções 2.4, 2.6.1.
2.3 Removendo cópias intermediárias pelo compilador
Considere a classe
class X { public: X();
E função
X Foo() {
Com uma abordagem direta, o retorno da função Foo()
é realizado copiando a instância do X
Mas os compiladores são capazes de remover a operação de cópia do código, o objeto é criado diretamente no ponto de chamada. Isso é chamado de otimização do valor de retorno (RVO). O RVO é usado pelos desenvolvedores de compiladores há algum tempo e atualmente está corrigido no padrão C ++ 11. Embora a decisão sobre o RVO seja tomada pelo compilador, o programador pode escrever código com base em seu uso. Para fazer isso, é desejável que a função tenha um ponto de retorno e o tipo da expressão retornada corresponda ao tipo do valor de retorno da função. Em alguns casos, é aconselhável definir um construtor fechado especial chamado “construtor computacional”, para obter mais detalhes, consulte [Dewhurst2]. O RVO também é discutido em [Meyers3] e [Guntheroth].
Os compiladores podem excluir cópias intermediárias em outras situações.
2.4 Implementação de semântica de deslocamento
A implementação da semântica de movimentação consiste em definir um construtor de movimentação que tenha um parâmetro do tipo rvalue-reference para a origem e um operador de atribuição de movimentação com o mesmo parâmetro.
Na biblioteca padrão do C ++ 11, o modelo da função de troca de estado é definido da seguinte maneira:
template<typename T> void swap(T& a, T& b) { T tmp(std::move(a)); a = std::move(b); b = std::move(tmp); }
De acordo com as regras para resolver sobrecargas de funções com parâmetros do tipo rvalue-reference (consulte o Apêndice A), no caso em que o tipo T
tenha um construtor em movimento e um operador de atribuição móvel, eles serão usados e não haverá alocação de recursos e cópias temporários. Caso contrário, o construtor de cópias e o operador de atribuição de cópias serão usados.
O uso da semântica da realocação evita a criação de cópias temporárias em um contexto muito mais amplo do que a função de troca de estado descrita acima. A semântica de movimento se aplica a qualquer valor rvalue, ou seja, um valor temporário, sem nome, bem como ao valor de retorno de uma função se ela foi criada localmente (incluindo lvalue) e o RVO não foi aplicado. Em todos esses casos, é garantido que o objeto de origem não possa ser utilizado de forma alguma após a movimentação. A semântica de movimentação também se aplica ao valor lvalue ao qual a transformação std::move()
é aplicada. Mas, neste caso, o programador é responsável por como os objetos de origem serão usados após a movimentação (exemplo: std::swap()
).
A biblioteca C ++ 11 padrão foi redesenhada, levando em consideração a semântica do movimento. Muitas classes adicionaram um construtor de movimentação e um operador de atribuição de movimentação, além de outras funções-membro, com parâmetros do tipo referência de valor. Por exemplo, std::vector<T>
possui uma versão sobrecarregada do void push_back(T&& src)
. Tudo isso permite, em muitos casos, evitar a criação de cópias temporárias.
A implementação da semântica de movimentação não cancela as definições da função de troca de estado para uma classe. Uma função de troca de estado especialmente definida pode ser mais eficiente que o padrão std::swap()
. Além disso, o construtor de movimentação e o operador de atribuição de movimentação são muito facilmente definidos usando a função de membro da troca de estados da seguinte maneira (variação da expressão de cópia e troca):
class X { public: X() noexcept {} void Swap(X& other) noexcept {} X(X&& src) noexcept : X() { Swap(src); } X& operator=(X&& src) noexcept { X tmp(std::move(src));
O construtor de movimentação e o operador de atribuição de movimentação são aquelas funções-membro para as quais é altamente desejável garantir que eles não noexcept
exceções e, portanto, sejam declarados como sem noexcept
. Isso permite otimizar algumas operações dos contêineres da biblioteca padrão sem violar a garantia estrita da segurança das exceções; consulte [Meyers3] e [Guntheroth] para obter detalhes. O modelo proposto fornece essa garantia, desde que o construtor padrão e a função membro da troca de estados não lançem exceções.
O padrão C ++ 11 fornece ao compilador a geração automática de um construtor em movimento e um operador de atribuição móvel.Para fazer isso, eles devem ser declarados usando a construção "=default"
.
class X { public: X(X&&) = default; X& operator=(X&&) = default;
As operações são implementadas aplicando sequencialmente a operação de movimentação às bases e membros da classe, se suportarem a movimentação, e copie as operações de outra forma. É claro que esta opção está longe de ser sempre aceitável. Os descritores brutos não se movem, mas geralmente não é possível copiá-los. Sob certas condições, o compilador pode gerar independentemente um construtor móvel semelhante e um operador de atribuição móvel, mas é melhor não usar essa oportunidade, essas condições são bastante confusas e podem mudar facilmente quando a classe é refinada. Veja [Meyers3] para detalhes.
Em geral, a implementação e o uso da semântica do deslocamento é uma "coisa sutil". O compilador pode aplicar cópias onde o programador espera uma mudança. Aqui estão algumas regras para eliminar ou pelo menos reduzir a probabilidade de tal situação.
- Se possível, use a proibição de cópia.
- Declare o construtor de movimentação e o operador de atribuição de movimentação como
noexcept
. - Implemente semântica de movimento para classes base e membros.
- Aplique a transformação
std::move()
aos parâmetros das funções do tipo rvalue reference.
A regra 2 foi discutida acima. 4 , rvalue- lvalue (. ). .
class B {
, . 6.2.1.
2.5 vs.
, RVO (. 2.3), , . ( ), , . , . C++11 - emplace()
, emplace_front()
, emplace_back()
, . , - — (variadic templates), . , C++11 — .
:
- , , .
- , , .
, .
std::vector<std::string> vs; vs.push_back(std::string(3, 'X'));
std::string
, . . , , . , [Meyers3].
2.6 Sumário
, , . - . . — : , . , , , . : , , «» .
: , , .NET Java. , Clone()
Duplicate()
.
- - , :
- .
- .
- - rvalue-.
.NET Java - , , .NET IClonable
. , .
3.
, . - , . , . Windows: , HANDLE
, COM-. DuplicateHandle()
, CloseHandle()
. COM- - IUnknown::AddRef()
IUnknown::Release()
. ATL ComPtr<>
, COM- . UNIX, C, _dup()
, .
C++11 std::shared_ptr<>
. , , , , , . , . std::shared_ptr<>
[Josuttis], [Meyers3].
: - , ( ). ( ) , . std::shared_ptr<>
std::weak_ptr<>
. . [Josuttis], [Meyers3].
- [Alexandrescu]. ( ) , [Schildt]. , .
( ) [Alger].
-. [Josuttis] [Alexandrescu].
- .NET Java. , , , .
4.
, C++ rvalue- . C++98 std::auto_ptr<>
, , , . , , ( ). C++11 rvalue- , , . C++11 std::auto_ptr><>
std::unique_ptr<>
. , [Josuttis], [Meyers3].
: - ( std::fstream
, etc.), ( std::thread
, std::unique_lock<>
, etc.). MFC , ( CFile
, CEvent
, CMutex
, etc.).
5. —
. , . , , , . , , , ( ) . , , , . ( ) , . , . — . 6.
, - -, « », - . - . , , , , - . «».
6. -
, - . , -. .
6.1.
- . , , :
- . , .
- .
- .
, , , . C++11 .
« » (resource acquisition is initialization, RAII). RAII ( ), ., [Dewhurst1]. «» RAII. , , , (immutable) RAII.
6.2.
, RAII, , , . - , , - . , , , . .
6.2.1.
, , , , :
- , .
- .
- .
- .
C++11 , , , . , - clear()
, , , . . , shrink_to_fit()
, , (. ).
, RAII, , , . , .
class X { public:
.
X x;
std::thread
.
2.4, - . , - - . .
class X {
:
X::X(X&& src) noexcept : X() { Swap(src); } X& X::operator=(X&& src) noexcept { X tmp(std::move(src));
- :
void X::Create() { X tmp();
, , , - . , , , . , .
- « », , . : , , ( ). : , . , : , , . , . [Sutter], [Sutter/Alexandrescu], [Meyers2].
, RAII .
6.2.2.
RAII . , , , , :
- , .
- .
- . , .
- .
- .
«» RAII, — . , , . 3. . «», .
6.2.3.
— . RAII , . , . , , ( -). - ( -). 6.2.1, .
6.3.
, - RAII, : . , , .
7.
, , , , . - -.
4 -:
- .
- .
- .
- .
. , - : , , - .
, . , , -, , .
- . . , (. 6.2.3). , (. 6.2.1). , . , , . , std::shared_ptr<>
.
Aplicações
. Rvalue-
Rvalue- C++ , , rvalue-. rvalue- T
T&&
.
:
class Int { int m_Value; public: Int(int val) : m_Value(val) {} int Get() const { return m_Value; } void Set(int val) { m_Value = val; } };
, rvalue- .
Int&& r0;
rvalue- ++ , lvalue. Um exemplo:
Int i(7); Int&& r1 = i;
rvalue:
Int&& r2 = Int(42);
lvalue rvalue-:
Int&& r4 = static_cast<Int&&>(i);
rvalue- ( ) std::move()
, ( <utility>
).
Rvalue rvalue , .
int&& r5 = 2 * 2;
rvalue- .
Int&& r = 7; std::cout << r.Get() << '\n';
Rvalue- .
Int&& r = 5; Int& x = r;
Rvalue- , . , rvalue-, rvalue .
void Foo(Int&&); Int i(7); Foo(i);
, rvalue rvalue- , . rvalue-.
, , , rvalue-, (ambiguous) rvalue .
void Foo(Int&&); void Foo(const Int&);
Int i(7); Foo(i);
: rvalue- lvalue.
Int&& r = 7; Foo(r);
, rvalue-, lvalue std::move()
. . 2.4.
++11, rvalue- — -. (lvalue/rvalue) this
.
class X { public: X(); void DoIt() &;
.
, ( std::string
, std::vector<>
, etc.) . — . , rvalue- . , , - , - , . , , , rvalue, lvalue. , rvalue. . , ( lvalue), RVO.
Referências
[Alexandrescu]
, . C++.: . do inglês - M .: LLC “I.D. », 2002.
[Guntheroth]
, . C++. .: . do inglês — .: «-», 2017.
[Josuttis]
, . C++: , 2- .: . do inglês - M .: LLC “I.D. », 2014.
[Dewhurst1]
, . C++. , 2- .: . do inglês — .: -, 2013.
[Dewhurst2]
, . C++. .: . do inglês — .: , 2012.
[Meyers1]
, . C++. 35 .: . do inglês — .: , 2000.
[Meyers2]
, . C++. 55 .: . do inglês — .: , 2014.
[Meyers3]
, . C++: 42 C++11 C ++14.: . do inglês - M .: LLC “I.D. », 2016.
[Sutter]
, . C++.: . do inglês — : «.. », 2015.
[Sutter/Alexandrescu]
, . , . ++.: . do inglês - M .: LLC “I.D. », 2015.
[Schildt]
, . C++.: . do inglês — .: -, 2005.
[Alger]
, . C++: .: . do inglês — .: « «», 1999.