Então, em que ponto uma classe se torna complexa demais para ser imutável?
Na minha opinião, não vale a pena se preocupar em tornar as turmas pequenas imutáveis em idiomas como o que você está mostrando. Estou usando aqui pequeno e não é complexo , porque mesmo que você adicione dez campos a essa classe e realmente realize operações sofisticadas, duvido que serão necessários kilobytes e muito menos megabytes e muito menos gigabytes, portanto, qualquer função usando instâncias de seu A classe pode simplesmente fazer uma cópia barata de todo o objeto para evitar modificar o original, se quiser evitar causar efeitos colaterais externos.
Estruturas de dados persistentes
Onde eu acho que o uso pessoal para imutabilidade é a grande estrutura de dados central que agrega um monte de dados pequeninos, como instâncias da classe que você está mostrando, como uma que armazena um milhão NamedThings
. Pertencendo a uma estrutura de dados persistente que é imutável e estando atrás de uma interface que permite apenas acesso somente leitura, os elementos que pertencem ao contêiner se tornam imutáveis sem que a classe de elemento ( NamedThing
) precise lidar com ele.
Cópias baratas
A estrutura de dados persistente permite que regiões dele sejam transformadas e tornadas únicas, evitando modificações no original sem precisar copiar a estrutura de dados em sua totalidade. Essa é a verdadeira beleza disso. Se você deseja escrever ingenuamente funções que evitam efeitos colaterais que inserem uma estrutura de dados que consome gigabytes de memória e modifica apenas o valor de memória de um megabyte, copie toda a loucura para evitar tocar na entrada e retornar um novo resultado. Ou copia gigabytes para evitar efeitos colaterais ou causar efeitos colaterais nesse cenário, fazendo com que você escolha entre duas opções desagradáveis.
Com uma estrutura de dados persistente, ele permite que você escreva uma função e evite fazer uma cópia de toda a estrutura de dados, exigindo apenas cerca de um megabyte de memória extra para a saída se sua função transformar apenas o valor de memória de um megabyte.
Carga
Quanto ao fardo, há um imediato, pelo menos no meu caso. Eu preciso dos construtores sobre os quais as pessoas estão falando ou "transientes", como eu os chamo, para poder expressar efetivamente transformações nessa estrutura de dados maciça sem tocá-la. Código como este:
void transform_stuff(MutList<Stuff>& stuff, int first, int last)
{
// Transform stuff in the range, [first, last).
for (; first != last; ++first)
transform(stuff[first]);
}
... então tem que ser escrito assim:
ImmList<Stuff> transform_stuff(ImmList<Stuff> stuff, int first, int last)
{
// Grab a "transient" (builder) list we can modify:
TransientList<Stuff> transient(stuff);
// Transform stuff in the range, [first, last)
// for the transient list.
for (; first != last; ++first)
transform(transient[first]);
// Commit the modifications to get and return a new
// immutable list.
return stuff.commit(transient);
}
Mas, em troca dessas duas linhas de código extras, agora é seguro chamar a função através de threads com a mesma lista original, não causa efeitos colaterais, etc. Também facilita muito tornar essa operação uma ação do usuário desfavorável, já que o desfazer pode apenas armazenar uma cópia rasa barata da lista antiga.
Segurança de exceção ou recuperação de erros
Nem todo mundo pode se beneficiar tanto quanto eu de estruturas de dados persistentes em contextos como esses (achei muito útil para eles em sistemas de desfazer e edição não destrutiva, que são conceitos centrais no meu domínio VFX), mas uma coisa se aplica a praticamente todo mundo a considerar é segurança de exceção ou recuperação de erros .
Se você deseja tornar segura a exceção da função de mutação original, ela precisa de lógica de reversão, para a qual a implementação mais simples requer a cópia de toda a lista:
void transform_stuff(MutList<Stuff>& stuff, int first, int last)
{
// Make a copy of the whole massive gigabyte-sized list
// in case we encounter an exception and need to rollback
// changes.
MutList<Stuff> old_stuff = stuff;
try
{
// Transform stuff in the range, [first, last).
for (; first != last; ++first)
transform(stuff[first]);
}
catch (...)
{
// If the operation failed and ran into an exception,
// swap the original list with the one we modified
// to "undo" our changes.
stuff.swap(old_stuff);
throw;
}
}
Nesse ponto, a versão mutável com exceção de segurança é ainda mais cara em termos de computação e sem dúvida ainda mais difícil de escrever corretamente do que a versão imutável usando um "construtor". E muitos desenvolvedores de C ++ apenas negligenciam a segurança de exceção e talvez isso seja bom para o domínio deles, mas no meu caso, eu gostaria de garantir que meu código funcione corretamente mesmo no caso de uma exceção (mesmo escrevendo testes que deliberadamente lançam exceções para testar exceção) segurança), e isso torna necessário que eu seja capaz de reverter quaisquer efeitos colaterais que uma função cause a meio caminho da função, se alguma coisa for lançada.
Quando você deseja ser protegido contra exceções e se recuperar de erros normalmente sem que seu aplicativo falhe e queime, você deve reverter / desfazer quaisquer efeitos colaterais que uma função possa causar no caso de um erro / exceção. E aí o construtor pode realmente economizar mais tempo do programador do que o custo, juntamente com o tempo computacional, porque: ...
Você não precisa se preocupar em reverter os efeitos colaterais em uma função que não causa nenhum efeito!
Então, voltando à questão fundamental:
Em que ponto as classes imutáveis se tornam um fardo?
Eles sempre são um fardo em idiomas que giram mais em torno da mutabilidade do que da imutabilidade, e é por isso que acho que você deve usá-los onde os benefícios superam significativamente os custos. Mas em um nível amplo o suficiente para estruturas de dados grandes o suficiente, acredito que há muitos casos em que é uma troca valiosa.
Também na minha, eu tenho apenas alguns tipos de dados imutáveis e todas elas são estruturas de dados enormes destinadas a armazenar um grande número de elementos (pixels de uma imagem / textura, entidades e componentes de um ECS e vértices / bordas / polígonos de uma malha).