Digamos que possuo o seguinte, class X
onde desejo retornar o acesso a um membro interno:
class Z
{
// details
};
class X
{
std::vector<Z> vecZ;
public:
Z& Z(size_t index)
{
// massive amounts of code for validating index
Z& ret = vecZ[index];
// even more code for determining that the Z instance
// at index is *exactly* the right sort of Z (a process
// which involves calculating leap years in which
// religious holidays fall on Tuesdays for
// the next thousand years or so)
return ret;
}
const Z& Z(size_t index) const
{
// identical to non-const X::Z(), except printed in
// a lighter shade of gray since
// we're running low on toner by this point
}
};
As duas funções de membro X::Z()
e X::Z() const
têm código idêntico dentro dos chavetas. Este é um código duplicado e pode causar problemas de manutenção para funções longas com lógica complexa .
Existe uma maneira de evitar essa duplicação de código?
Respostas:
Para uma explicação detalhada, consulte o título "Evitar duplicação de funções
const
e não pertencentes aconst
membros", na p. 23, no item 3 "Useconst
sempre que possível", em C ++ efetivo , 3d ed por Scott Meyers, ISBN-13: 9780321334879.Aqui está a solução de Meyers (simplificada):
As duas transmissões e chamada de função podem ser feias, mas estão corretas. Meyers tem uma explicação completa do porquê.
fonte
get()const
retornar algo que foi definido como um objeto const, então não deve haver uma versão não-constget()
. Na verdade, meu pensamento sobre isso mudou ao longo do tempo: a solução do modelo é a única maneira de evitar duplicação e obter a correção const constante do compilador; portanto, pessoalmente, eu não usaria mais umconst_cast
para evitar a duplicação de código, eu escolheria colocar o código enganado em um modelo de função ou deixando-o enganado.template<typename T> const T& constant(T& _) { return const_cast<const T&>(_); }
etemplate<typename T> T& variable(const T& _) { return const_cast<T&>(_); }
. Então você pode fazer:return variable(constant(*this).get());
Sim, é possível evitar a duplicação de código. Você precisa usar a função de membro const para ter a lógica e fazer com que a função de membro não const chame a função de membro const e libere novamente o valor de retorno para uma referência que não seja const (ou ponteiro se as funções retornarem um ponteiro):
NOTA: É importante que você NÃO coloque a lógica na função não-const e faça com que a função const chame a função não-const - isso pode resultar em um comportamento indefinido. O motivo é que uma instância de classe constante é convertida como uma instância não constante. A função de membro não const pode modificar acidentalmente a classe, que os estados padrão do C ++ resultarão em um comportamento indefinido.
fonte
O C ++ 17 atualizou a melhor resposta para esta pergunta:
Isso tem as vantagens de que:
volatile
por acidente, masvolatile
é um classificador raro)Se você deseja seguir o caminho completo da dedução, isso pode ser realizado com uma função auxiliar
Agora você não pode nem errar
volatile
, e o uso parecefonte
f()
retornar emT
vez deT&
.f()
retornaT
, não queremos ter duas sobrecargas, apenas aconst
versão é suficiente.shared_ptr
. Então, o que eu realmente precisava era de algo parecidoas_mutable_ptr
com oas_mutable
acima, exceto que ele pega e retorna aeshared_ptr
usa emstd::const_pointer_cast
vez deconst_cast
.T const*
, isso vincularia aoT const* const&&
invés de vincularT const* const&
(pelo menos nos meus testes). Eu tive que adicionar uma sobrecarga paraT const*
como o tipo de argumento para métodos retornando um ponteiro.Acho que a solução de Scott Meyers pode ser aprimorada no C ++ 11 usando uma função auxiliar de tempate. Isso torna a intenção muito mais óbvia e pode ser reutilizada para muitos outros getters.
Esta função auxiliar pode ser usada da seguinte maneira.
O primeiro argumento é sempre o ponteiro this. O segundo é o ponteiro para a função de membro a ser chamada. Depois disso, uma quantidade arbitrária de argumentos adicionais pode ser passada para que eles possam ser encaminhados para a função. Isso precisa do C ++ 11 devido aos modelos variados.
fonte
std::remove_bottom_const
que concordarstd::remove_const
.const_cast
. Você pode criargetElement
um modelo em si e usar a característica do tipo dentro dosmpl::conditional
tipos que você precisa, comoiterator
s ouconstiterator
s, se necessário. O verdadeiro problema é como gerar uma versão const de um método quando essa parte da assinatura não pode ser modelada?std::remove_const<int const&>
isint const &
(remova aconst
qualificação de nível superior ), daí a ginásticaNonConst<T>
desta resposta. O putativostd::remove_bottom_const
pode remover aconst
qualificação de nível inferior e fazer exatamente o queNonConst<T>
faz aqui:std::remove_bottom_const<int const&>::type
=>int&
.getElement
estiver sobrecarregada. Em seguida, o ponteiro da função não pode ser resolvido sem fornecer explicitamente os parâmetros do modelo. Por quê?likeConstVersion(TObj const* obj, TConstReturn (TObj::*memFun)(TArgs...) const, TArgs&&... args) { return const_cast<typename NonConst<TConstReturn>::type>((obj->*memFun)(std::forward<TArgs>(args)...)); }
Complete: gist.github.com/BlueSolei/bca26a8590265492e2f2760d3cefcf83Um pouco mais detalhado do que Meyers, mas eu posso fazer isso:
O método private tem a propriedade indesejável de retornar um Z não-const e para uma instância const, e é por isso que é privado. Métodos privados podem quebrar invariantes da interface externa (neste caso, a invariável desejada é "um objeto const não pode ser modificado através de referências obtidas através dele para objetos que possui-a").
Observe que os comentários fazem parte do padrão - a interface de _getZ especifica que nunca é válido chamá-lo (além dos acessadores, obviamente): não há benefício concebível em fazê-lo, porque é mais um caractere a ser digitado e não será resultar em código menor ou mais rápido. Chamar o método é equivalente a chamar um dos acessadores com um const_cast, e você também não deseja fazer isso. Se você está preocupado em tornar os erros óbvios (e esse é um objetivo justo), chame-o de const_cast_getZ em vez de _getZ.
A propósito, eu aprecio a solução de Meyers. Não tenho objeções filosóficas a isso. Pessoalmente, porém, prefiro um pouco de repetição controlada e um método privado que só deve ser chamado em determinadas circunstâncias bem controladas, em vez de um método que se parece com ruído de linha. Escolha o seu veneno e fique com ele.
[Editar: Kevin apontou, com razão, que _getZ pode querer chamar outro método (digamos generateZ), que é constantemente especializado da mesma maneira que getZ. Nesse caso, _getZ verá uma const Z & e terá que const_cast antes de retornar. Isso ainda é seguro, pois o acessador do clichê monitora tudo, mas não é extremamente óbvio que é seguro. Além disso, se você fizer isso e depois alterar o generateZ para sempre retornar const, também será necessário alterar getZ para sempre retornar const, mas o compilador não informará o que você faz.
Esse último ponto sobre o compilador também se aplica ao padrão recomendado por Meyers, mas o primeiro ponto sobre um const_cast não óbvio não é. Portanto, pensando bem, se _getZ precisar de um const_cast para seu valor de retorno, esse padrão perderá muito do seu valor sobre o de Meyers. Como também sofre desvantagens em comparação com as de Meyers, acho que mudaria para a dele nessa situação. A refatoração de um para o outro é fácil - não afeta nenhum outro código válido na classe, pois apenas o código inválido e o clichê chamam _getZ.]
fonte
something
na_getZ()
função for uma variável de instância? O compilador (ou pelo menos alguns compiladores) reclamará que, como_getZ()
é const, qualquer variável de instância referenciada também é const. Então,something
seria const (seria do tipoconst Z&
) e não poderia ser convertido emZ&
. Na minha experiência (reconhecidamente um pouco limitada), na maioria das vezessomething
é uma variável de instância em casos como este.const_cast
. Ele pretendia ser um marcador de posição para o código necessário para obter um retorno não const do objeto const, não como marcador de posição para o que teria sido no getter duplicado. Portanto, "algo" não é apenas uma variável de instância.Boa pergunta e boas respostas. Eu tenho outra solução, que não usa moldes:
No entanto, possui a feiúra de exigir um membro estático e a necessidade de usar a
instance
variável dentro dele.Não considerei todas as implicações possíveis (negativas) dessa solução. Por favor me avise se houver.
fonte
auto get(std::size_t i) -> auto(const), auto(&&)
. Por quê '&&'? Ahh, então posso dizer:auto foo() -> auto(const), auto(&&) = delete;
this
palavras-chave. Eu sugiro quetemplate< typename T > auto myfunction(T this, t args) -> decltype(ident)
a palavra-chave this seja reconhecida como o argumento implícito da instância do objeto e permita que o compilador reconheça que myfunction é um membro ouT
.T
será deduzido automaticamente no site da chamada, que sempre será o tipo de turma, mas com qualificação cv gratuita.const_cast
) de permitir retornariterator
econst_iterator
.static
pode ser feito no escopo do arquivo, em vez do escopo da classe. :-)Você também pode resolver isso com modelos. Essa solução é um pouco feia (mas a feiúra está oculta no arquivo .cpp), mas fornece verificação de consistência do compilador e nenhuma duplicação de código.
arquivo .h:
arquivo .cpp:
A principal desvantagem que vejo é que, como toda a implementação complexa do método está em uma função global, você precisa se apossar dos membros do X usando métodos públicos como GetVector () acima (dos quais sempre é necessário haver um versão const e non-const) ou você pode tornar essa função um amigo. Mas eu não gosto de amigos.
[Editar: inclusão desnecessária removida do cstdio adicionada durante o teste.]
fonte
const_cast
(que pode ser acidentalmente usado para separar algo que na verdade deveria ser const ou algo que não é).Que tal mover a lógica para um método privado e fazer apenas o item "obter a referência e retornar" dentro dos getters? Na verdade, eu ficaria bastante confuso sobre a conversão estática e const dentro de uma função getter simples, e consideraria isso feio, exceto em circunstâncias extremamente raras!
fonte
É trapaça usar o pré-processador?
Não é tão sofisticado quanto modelos ou elencos, mas torna sua intenção ("essas duas funções devem ser idênticas") bastante explícita.
fonte
É surpreendente para mim que haja tantas respostas diferentes, mas quase todas dependam de uma mágica pesada de modelos. Os modelos são poderosos, mas às vezes as macros os vencem em concisão. A versatilidade máxima é frequentemente alcançada combinando ambos.
Eu escrevi uma macro
FROM_CONST_OVERLOAD()
que pode ser colocada na função não-const para invocar a função const.Exemplo de uso:
Implementação simples e reutilizável:
Explicação:
Conforme publicado em muitas respostas, o padrão típico para evitar duplicação de código em uma função de membro que não seja const é:
Muito deste padrão pode ser evitado usando inferência de tipo. Primeiro,
const_cast
pode ser encapsulado emWithoutConst()
, que infere o tipo de seu argumento e remove o const-qualifier. Segundo, uma abordagem semelhante pode ser usadaWithConst()
para qualificar constantemente othis
ponteiro, o que permite chamar o método sobrecarregado de const.O restante é uma macro simples que prefixa a chamada com a qualificação correta
this->
e remove a const do resultado. Como a expressão usada na macro é quase sempre uma chamada de função simples com argumentos encaminhados 1: 1, as desvantagens de macros, como a avaliação múltipla, não surgem. As reticências e__VA_ARGS__
também podem ser usadas, mas não devem ser necessárias porque vírgulas (como separadores de argumentos) ocorrem entre parênteses.Essa abordagem tem vários benefícios:
FROM_CONST_OVERLOAD( )
const_iterator
,std::shared_ptr<const T>
, etc.). Para isso, basta sobrecarregarWithoutConst()
os tipos correspondentes.Limitações: esta solução é otimizada para cenários em que a sobrecarga não const está fazendo exatamente o mesmo que a sobrecarga const, para que os argumentos possam ser encaminhados 1: 1. Se sua lógica for diferente e você não estiver chamando a versão const via
this->Method(args)
, considere outras abordagens.fonte
Para aqueles (como eu) que
aqui está outra tomada:
É basicamente uma mistura das respostas de @Pait, @DavidStone e @ sh1 ( EDIT : e uma melhoria do @cdhowie). O que ele adiciona à tabela é que você se safa com apenas uma linha extra de código que simplesmente nomeia a função (mas nenhum argumento ou duplicação de tipo de retorno):
Nota: O gcc falha ao compilar isso antes da 8.1, clang-5 e superior, assim como o MSVC-19, estão felizes (de acordo com o explorador do compilador ).
fonte
decltype()
s também não deveriam estar usandostd::forward
os argumentos para garantir que estamos usando o tipo de retorno correto no caso em que temos sobrecargasget()
que requerem diferentes tipos de referência?NON_CONST
macro deduz incorretamente o tipo de retornoconst_cast
es no tipo incorreto devido à falta de encaminhamento nosdecltype(func(a...))
tipos. Substituí-los pordecltype(func(std::forward<T>(a)...))
resolve isso . (Há apenas um erro de vinculador porque eu nunca definiu qualquer um dos declaradosX::get
sobrecargas.)Aqui está uma versão C ++ 17 da função auxiliar estática do modelo, com um teste SFINAE opcional.
Versão completa: https://godbolt.org/z/mMK4r3
fonte
Eu vim com uma macro que gera pares de funções const / non-const automaticamente.
Veja o final da resposta para a implementação.
O argumento de
MAYBE_CONST
é duplicado. Na primeira cópia,CV
é substituído por nada; e na segunda cópia é substituída porconst
.Não há limite para quantas vezes
CV
podem aparecer no argumento da macro.Há um pequeno inconveniente. Se
CV
aparecer entre parênteses, esse par de parênteses deve ser prefixado comCV_IN
:Implementação:
Implementação pré-C ++ 20 que não suporta
CV_IN
:fonte
Normalmente, as funções-membro para as quais você precisa de versões const e não-const são getters e setters. Na maioria das vezes eles são one-liners, portanto a duplicação de código não é um problema.
fonte
Eu fiz isso por um amigo que justificou o uso de
const_cast
... sem saber, provavelmente teria feito algo assim (não muito elegante):fonte
Eu sugeriria um modelo de função estática de auxiliar particular, assim:
fonte
Este artigo do DDJ mostra uma maneira de usar a especialização de modelo que não requer que você use const_cast. Para uma função tão simples, ela realmente não é necessária.
O boost :: any_cast (em um ponto, isso não acontece mais) usa um const_cast da versão const chamando a versão não-const para evitar duplicação. Você não pode impor semântica const na versão não-const, portanto, você deve ser muito cuidado com isso.
No final, alguma duplicação de código é aceitável, desde que os dois trechos estejam diretamente em cima um do outro.
fonte
Para adicionar à solução fornecida pelo jwfearn e pelo kevin, aqui está a solução correspondente quando a função retorna shared_ptr:
fonte
Não encontrei o que estava procurando, então eu rolei alguns dos meus ...
Este é um pouco prolixo, mas tem a vantagem de lidar com muitos métodos sobrecarregados com o mesmo nome (e tipo de retorno) de uma só vez:
Se você tiver apenas um
const
método por nome, mas ainda houver muitos métodos para duplicar, poderá preferir o seguinte:Infelizmente, isso é interrompido assim que você começa a sobrecarregar o nome (a lista de argumentos do argumento do ponteiro de função parece não ter sido resolvida nesse momento, portanto, não é possível encontrar uma correspondência para o argumento da função). Embora você possa criar um modelo para isso também:
Mas os argumentos de referência para o
const
método não coincidem com os argumentos aparentemente com valor agregado no modelo e ele é quebrado.Não sei por que.Aqui está o porquê .fonte