Como faço para remover a duplicação de código entre const semelhante e funções membro não-const?

StackOverflow https://stackoverflow.com/questions/123758

Pergunta

Vamos dizer que eu tenho o seguinte class X onde eu quero voltar 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
    }
};

Os dois Estados funções X::Z() e X::Z() const têm código idêntico dentro das chaves. Este é o código duplicado e pode causar problemas de manutenção por longos funções com lógica complexa .

Existe uma maneira de evitar esta duplicação de código?

Foi útil?

Solução 2

Sim, é possível evitar a duplicação de código. Você precisa usar a função de membro const ter a lógica e ter a chamada de função de membro não-const a função de membro const e re-lançar o valor de retorno para uma referência não-const (ou ponteiro se as funções retorna um ponteiro):

class X
{
   std::vector<Z> vecZ;

public:
   const Z& Z(size_t index) const
   {
      // same really-really-really long access 
      // and checking code as in OP
      // ...
      return vecZ[index];
   }

   Z& Z(size_t index)
   {
      // One line. One ugly, ugly line - but just one line!
      return const_cast<Z&>( static_cast<const X&>(*this).Z(index) );
   }

 #if 0 // A slightly less-ugly version
   Z& Z(size_t index)
   {
      // Two lines -- one cast. This is slightly less ugly but takes an extra line.
      const X& constMe = *this;
      return const_cast<Z&>( constMe.Z(index) );
   }
 #endif
};

NOTA: É importante que você faça não colocar a lógica na função não-const e ter a chamada da função const a função não-const - lo podem resultar em comportamento indefinido. A razão é que uma instância de classe constante fica fundido como um exemplo não constante. A função de membro não-const pode acidentalmente modificar a classe, que os estados de C ++ padrão irá resultar em comportamento indefinido.

Outras dicas

Para uma explicação detalhada, consulte o título "evitar a duplicação em const e Não-const função de membro", na p. 23, no item 3 "Use const sempre que possível," em Effective C ++ , 3d ed por Scott Meyers, ISBN-13:. 9780321334879

text alt

Aqui está uma solução Meyers' (simplificado):

struct C {
  const char & get() const {
    return c;
  }
  char & get() {
    return const_cast<char &>(static_cast<const C &>(*this).get());
  }
  char c;
};

Os dois elencos e chamada de função pode ser feio, mas ele está correto. Meyers tem uma explicação completa por isso.

Eu acho solução Scott Meyers' pode ser melhorado em C ++ 11 usando uma função tempate ajudante. Isso faz com a intenção muito mais óbvia e pode ser reutilizado para muitas outras getters.

template <typename T>
struct NonConst {typedef T type;};
template <typename T>
struct NonConst<T const> {typedef T type;}; //by value
template <typename T>
struct NonConst<T const&> {typedef T& type;}; //by reference
template <typename T>
struct NonConst<T const*> {typedef T* type;}; //by pointer
template <typename T>
struct NonConst<T const&&> {typedef T&& type;}; //by rvalue-reference

template<typename TConstReturn, class TObj, typename... TArgs>
typename NonConst<TConstReturn>::type likeConstVersion(
   TObj const* obj,
   TConstReturn (TObj::* memFun)(TArgs...) const,
   TArgs&&... args) {
      return const_cast<typename NonConst<TConstReturn>::type>(
         (obj->*memFun)(std::forward<TArgs>(args)...));
}

Esta função auxiliar pode ser usado da seguinte maneira.

struct T {
   int arr[100];

   int const& getElement(size_t i) const{
      return arr[i];
   }

   int& getElement(size_t i) {
      return likeConstVersion(this, &T::getElement, i);
   }
};

O primeiro argumento é sempre o ponteiro isso. A segunda é o ponteiro para a função de membro de chamada. Depois que uma quantidade arbitrária de argumentos adicionais podem ser passados ??para que possam ser encaminhados para a função. Isso precisa C ++ 11 por causa dos modelos variádicos.

Um pouco mais detalhado do que Meyers, mas eu poderia fazer isso:

class X {

    private:

    // This method MUST NOT be called except from boilerplate accessors.
    Z &_getZ(size_t index) const {
        return something;
    }

    // boilerplate accessors
    public:
    Z &getZ(size_t index)             { return _getZ(index); }
    const Z &getZ(size_t index) const { return _getZ(index); }
};

O método privado tem a propriedade indesejável que ele retorna um Z não-const & para uma instância const, que é por isso que é privado. métodos particulares podem quebrar invariantes do interface externa (neste caso o invariante desejado é "um objecto constante não pode ser modificada por meio de referências obtidas através dele para objectos tem-a").

Note que os comentários são parte do padrão - especifica de interface da _getZ que nunca é válida para chamá-lo (para além dos assessores, obviamente): não há benefício concebível para fazê-lo de qualquer maneira, porque é mais um personagem para digitar e não irá resultar em código menor ou mais rápido. Chamar o método é equivalente a chamar um dos assessores com um const_cast, e você não iria querer fazer isso também. Se você está preocupado com a cometer erros óbvios (e isso é um objetivo justo), então chamá-lo const_cast_getZ vez de _getZ.

A propósito, eu aprecio a solução da Meyers. Não tenho nenhuma objeção filosófica a ele. Pessoalmente, porém, eu prefiro um pouco de repetição controlada, e um método particular que só deve ser chamado em determinadas circunstâncias controlados firmemente, ao longo de um método que se parece com o ruído de linha. Escolha o seu veneno e ficar com ela.

[Edit: Kevin justamente sublinhou que _getZ pode querer chamar um método mais (dizer generateZ) que é const especializados da mesma forma Getz é. Neste caso, _getZ veria um const Z & e tem que const_cast-lo antes do retorno. Isso ainda é segura, uma vez que o clichê acessor políticas tudo, mas não é extraordinariamente óbvio que ele é seguro. Além disso, se você fizer isso e depois mudar generateZ a retornar sempre const, então você também precisa mudar Getz para retornar sempre const, mas o compilador não irá dizer-lhe que você faz.

Este último ponto sobre o compilador também é verdadeiro do padrão recomendado pela Meyers, mas o primeiro ponto sobre um const_cast não-óbvia não é. Assim, no balanço Eu acho que se _getZ acaba por precisar de uma const_cast pelo seu valor de retorno, então este padrão perde muito de seu valor ao longo do Meyers do. Uma vez que também sofre desvantagens em comparação com Meyers do, eu acho que vou mudar para sua nessa situação. Refatoração de um para o outro é fácil - ela não afeta qualquer outro código válido na classe, uma vez que apenas código inválido eo clichê chama _getZ]

.

C ++ 17 actualiza a melhor resposta para esta pergunta:

T const & f() const {
    return something_complicated();
}
T & f() {
    return const_cast<T &>(std::as_const(*this).f());
}

Isto tem as vantagens que:

  • É óbvio que está acontecendo
  • Tem código mínimo de sobrecarga - ele se encaixa em uma única linha
  • É difícil conseguir errado (só pode jogar fora volatile por acidente, mas volatile é um qualificador raro)

Se você quiser ir a rota de dedução integral, em seguida, que pode ser conseguido por meio de uma função auxiliar

template<typename T>
constexpr T & as_mutable(T const & value) noexcept {
    return const_cast<T &>(value);
}
template<typename T>
void as_mutable(T const &&) = delete;

Agora você não pode mesmo atrapalhar volatile, e os olhares de uso como

T & f() {
    return as_mutable(std::as_const(*this).f());
}

questão de Nice e respostas agradáveis. Eu tenho uma outra solução, que não usa moldes:

class X {

private:

    std::vector<Z> v;

    template<typename InstanceType>
    static auto get(InstanceType& instance, std::size_t i) -> decltype(instance.get(i)) {
        // massive amounts of code for validating index
        // the instance variable has to be used to access class members
        return instance.v[i];
    }

public:

    const Z& get(std::size_t i) const {
        return get(*this, i);
    }

    Z& get(std::size_t i) {
        return get(*this, i);
    }

};

No entanto, ele tem a feiúra de exigir um membro estático e a necessidade de utilizar a variável instance dentro dela.

Eu não considerou todas as possíveis implicações (negativos) desta solução. Por favor me avise se houver.

Você também pode resolver isso com modelos. Esta solução é um pouco feio (mas a feiúra está escondido no arquivo .cpp) mas fornece compilador verificação de constness, e sem duplicação de código.

H arquivo:

#include <vector>

class Z
{
    // details
};

class X
{
    std::vector<Z> vecZ;

public:
    const std::vector<Z>& GetVector() const { return vecZ; }
    std::vector<Z>& GetVector() { return vecZ; }

    Z& GetZ( size_t index );
    const Z& GetZ( size_t index ) const;
};

arquivo.cpp:

#include "constnonconst.h"

template< class ParentPtr, class Child >
Child& GetZImpl( ParentPtr parent, size_t index )
{
    // ... massive amounts of code ...

    // Note you may only use methods of X here that are
    // available in both const and non-const varieties.

    Child& ret = parent->GetVector()[index];

    // ... even more code ...

    return ret;
}

Z& X::GetZ( size_t index )
{
    return GetZImpl< X*, Z >( this, index );
}

const Z& X::GetZ( size_t index ) const
{
    return GetZImpl< const X*, const Z >( this, index );
}

A principal desvantagem que vejo é que, porque todo o complexo implementação do método está em uma função global, você quer necessidade de se apossar dos membros do X usando métodos públicos como getVector () acima (de que há sempre necessidade para ser uma versão const e não-const) ou você poderia fazer esta função um amigo. Mas eu não gosto de amigos.

[Editar:. Desnecessários removidos incluem de cstdio adicionado durante o teste]

Como sobre se mover a lógica em um método particular, e apenas fazer a "obter a referência e voltar" coisas dentro dos getters? Na verdade, eu seria bastante confuso sobre os moldes static e const dentro de uma função getter simples, e eu considero que feio, exceto para circunstâncias extremamente raras!

É batota usar o pré-processador?

struct A {

    #define GETTER_CORE_CODE       \
    /* line 1 of getter code */    \
    /* line 2 of getter code */    \
    /* .....etc............. */    \
    /* line n of getter code */       

    // ^ NOTE: line continuation char '\' on all lines but the last

   B& get() {
        GETTER_CORE_CODE
   }

   const B& get() const {
        GETTER_CORE_CODE
   }

   #undef GETTER_CORE_CODE

};

Não é tão extravagante como modelos ou moldes, mas faz a sua intenção ( "essas duas funções devem ser idênticos") bastante explícito.

Normalmente, as funções de membro para o qual você precisa const e versões não-const são getters e setters. Na maioria das vezes eles são one-liners assim a duplicação de código não é um problema.

Eu fiz isso por um amigo que legitimamente justificado o uso de const_cast ... não saber sobre isso, eu provavelmente teria feito algo assim (não é realmente elegante):

#include <iostream>

class MyClass
{

public:

    int getI()
    {
        std::cout << "non-const getter" << std::endl;
        return privateGetI<MyClass, int>(*this);
    }

    const int getI() const
    {
        std::cout << "const getter" << std::endl;
        return privateGetI<const MyClass, const int>(*this);
    }

private:

    template <class C, typename T>
    static T privateGetI(C c)
    {
        //do my stuff
        return c._i;
    }

    int _i;
};

int main()
{
    const MyClass myConstClass = MyClass();
    myConstClass.getI();

    MyClass myNonConstClass;
    myNonConstClass.getI();

    return 0;
}

Eu sugiro um modelo privado função auxiliar estático, como este:

class X
{
    std::vector<Z> vecZ;

    // ReturnType is explicitly 'Z&' or 'const Z&'
    // ThisType is deduced to be 'X' or 'const X'
    template <typename ReturnType, typename ThisType>
    static ReturnType Z_impl(ThisType& self, size_t index)
    {
        // massive amounts of code for validating index
        ReturnType ret = self.vecZ[index];
        // even more code for determining, blah, blah...
        return ret;
    }

public:
    Z& Z(size_t index)
    {
        return Z_impl<Z&>(*this, index);
    }
    const Z& Z(size_t index) const
    {
        return Z_impl<const Z&>(*this, index);
    }
};

Para aqueles (como eu) que

  • uso c ++ 17
  • deseja adicionar o menor quantidade de clichê / repetição e
  • não se importa com makros (enquanto espera por meta-classes ...),

aqui é uma outra opinião:

#include <utility>
#include <type_traits>

template <typename T> struct NonConst;
template <typename T> struct NonConst<T const&> {using type = T&;};
template <typename T> struct NonConst<T const*> {using type = T*;};

#define NON_CONST(func)                                                     \
    template <typename... T>                                                \
    auto func(T&&... a) -> typename NonConst<decltype(func(a...))>::type {  \
        return const_cast<decltype(func(a...))>(                            \
            std::as_const(*this).func(std::forward<T>(a)...));              \
    }

É basicamente uma combinação das respostas de @Pait, @DavidStone e @ sh1. O que acrescenta à mesa é que você fugir com apenas uma linha extra de código que simplesmente nomes da função (mas nenhum argumento ou tipo de retorno de duplicação):

class X
{
    const Z& get(size_t index) const { ... }
    NON_CONST(get)
};

Nota: gcc falha ao compilar isto antes de 8.1, tinido-5 e para cima, bem como MSVC-19 são satisfeitos (de acordo com a o compilador explorador ).

É surpreendente para mim que há tantas respostas diferentes, mas quase todos dependem de magia modelo pesado. Os modelos são poderosos, mas às vezes macros vencê-los na concisão. Máxima versatilidade é muitas vezes conseguida através da combinação de ambos.

Eu escrevi um FROM_CONST_OVERLOAD() macro que pode ser colocado na função de não-const para invocar a função const.

Exemplo de uso:

class MyClass
{
private:
    std::vector<std::string> data = {"str", "x"};

public:
    // Works for references
    const std::string& GetRef(std::size_t index) const
    {
        return data[index];
    }

    std::string& GetRef(std::size_t index)
    {
        return FROM_CONST_OVERLOAD( GetRef(index) );
    }


    // Works for pointers
    const std::string* GetPtr(std::size_t index) const
    {
        return &data[index];
    }

    std::string* GetPtr(std::size_t index)
    {
        return FROM_CONST_OVERLOAD( GetPtr(index) );
    }
};

Simples e implementação reutilizável:

template <typename T>
T& WithoutConst(const T& ref)
{
    return const_cast<T&>(ref);
}

template <typename T>
T* WithoutConst(const T* ptr)
{
    return const_cast<T*>(ptr);
}

template <typename T>
const T* WithConst(T* ptr)
{
    return ptr;
}

#define FROM_CONST_OVERLOAD(FunctionCall) \
  WithoutConst(WithConst(this)->FunctionCall)

Explicação:

Como postou em muitas respostas, o padrão típico para a duplicação de código evitar em uma função de membro não-const é esta:

return const_cast<Result&>( static_cast<const MyClass*>(this)->Method(args) );

Um monte de este clichê pode ser evitado usando o tipo de inferência. Primeiro, const_cast pode ser encapsulado em WithoutConst(), que infere o tipo de seu argumento e remove o const-qualifier. Em segundo lugar, uma abordagem semelhante pode ser usada em WithConst() para const-qualificar o ponteiro this, que permite chamar o método sobrecarregado-const.

O resto é uma macro simples que prefixos a chamada com o this-> corretamente qualificado e remove const a partir do resultado. Desde a expressão usada na macro é quase sempre uma chamada de função simples, com 1: 1 argumentos encaminhados, inconvenientes de macros, como a avaliação múltipla não pontapé nas reticências e __VA_ARGS__ também poderia ser usado, mas não deve ser necessário porque vírgulas (. como separadores de argumentos) ocorrem dentro de parênteses.

Esta abordagem tem várias vantagens:

  • sintaxe Minimal e natural - só embrulhar a chamada em FROM_CONST_OVERLOAD( )
  • Sem função membro extra necessário
  • Compatível com C ++ 98
  • implementação simples, nenhuma modelo metaprogramming e zero dependências
  • Extensible: outras relações const pode ser adicionado (como const_iterator, std::shared_ptr<const T>, etc.). Para isso, basta sobrecarregar WithoutConst() para os tipos correspondentes.

Limitações: esta solução é otimizada para cenários em que a sobrecarga não-const está fazendo exatamente a mesma que a sobrecarga de const, de modo que os argumentos podem ser encaminhadas 1: 1. Se seus difere de lógica e você não está chamando a versão const via this->Method(args), você pode considerar outras abordagens.

este artigo DDJ mostra uma maneira de usar especialização de modelo que não requer que você use const_cast. Para tal função simples que realmente não é necessário embora.

boost :: any_cast (em um ponto, não faz mais nada) usa um const_cast da versão const chamando a versão não-const para evitará a duplicação. Você não pode impor semântica const na versão não-const embora assim que você tem que ser muito cuidado com isso.

No final alguns duplicação de código é tudo bem, enquanto os dois trechos estão diretamente em cima uns dos outros.

Para adicionar à jwfearn solução e kevin fornecido, aqui está a solução correspondente quando a função retorna shared_ptr:

struct C {
  shared_ptr<const char> get() const {
    return c;
  }
  shared_ptr<char> get() {
    return const_pointer_cast<char>(static_cast<const C &>(*this).get());
  }
  shared_ptr<char> c;
};

Não encontrou o que estava procurando, então eu rolou um par de minha própria ...

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:

struct C {
  int x[10];

  int const* getp() const { return x; }
  int const* getp(int i) const { return &x[i]; }
  int const* getp(int* p) const { return &x[*p]; }

  int const& getr() const { return x[0]; }
  int const& getr(int i) const { return x[i]; }
  int const& getr(int* p) const { return x[*p]; }

  template<typename... Ts>
  auto* getp(Ts... args) {
    auto const* p = this;
    return const_cast<int*>(p->getp(args...));
  }

  template<typename... Ts>
  auto& getr(Ts... args) {
    auto const* p = this;
    return const_cast<int&>(p->getr(args...));
  }
};

Se você tem apenas um método const por nome, mas ainda abundância de métodos para duplicar, então você pode preferir este:

  template<typename T, typename... Ts>
  auto* pwrap(T const* (C::*f)(Ts...) const, Ts... args) {
    return const_cast<T*>((this->*f)(args...));
  }

  int* getp_i(int i) { return pwrap(&C::getp_i, i); }
  int* getp_p(int* p) { return pwrap(&C::getp_p, p); }

Infelizmente Isso quebra assim que você começar a sobrecarregar o nome (lista de argumentos do argumento ponteiro de função parece ser não resolvido naquele momento, por isso não pode encontrar uma correspondência para o argumento da função). Embora você possa template o caminho para sair disso também:

  template<typename... Ts>
  auto* getp(Ts... args) { return pwrap<int, Ts...>(&C::getp, args...); }

Mas os argumentos referência ao método const falhar para o jogo contra os aparentemente argumentos por valor para o modelo e ele quebra. Não sei por que. Aqui está o porquê .

Licenciado em: CC-BY-SA com atribuição
Não afiliado a StackOverflow
scroll top