Estratégias Const C ++ DRY

14

Para evitar duplicação não-trivial relacionada à const C ++, existem casos em que const_cast funcionaria, mas uma função const privada retornando non-const não funcionaria?

No item efetivo C ++ de Scott Meyers , 3, ele sugere que um const_cast combinado com uma conversão estática pode ser uma maneira eficaz e segura de evitar código duplicado, por exemplo,

const void* Bar::bar(int i) const
{
  ...
  return variableResultingFromNonTrivialDotDotDotCode;
}
void* Bar::bar(int i)
{
  return const_cast<void*>(static_cast<const Bar*>(this)->bar(i));
}

Meyers continua explicando que ter a função const chamar a função non-const é perigoso.

O código abaixo é um contra-exemplo mostrando:

  • Ao contrário da sugestão de Meyers, às vezes o const_cast combinado com um elenco estático é perigoso
  • às vezes, ter a função const chamar a não-const é menos perigoso
  • às vezes nos dois sentidos, usando um const_cast, oculta erros de compilador potencialmente úteis
  • evitar um const_cast e ter um membro privado const adicional retornando um não-const é outra opção

Alguma das estratégias const_cast de evitar duplicação de código é considerada uma boa prática? Você prefere a estratégia de método privado? Existem casos em que const_cast funcionaria, mas um método privado não? Existem outras opções (além da duplicação)?

Minha preocupação com as estratégias const_cast é que, mesmo que o código esteja correto quando gravado, mais tarde durante a manutenção, o código poderá ficar incorreto e o const_cast ocultará um erro útil do compilador. Parece que uma função privada comum é geralmente mais segura.

class Foo
{
  public:
    Foo(const LongLived& constLongLived, LongLived& mutableLongLived)
    : mConstLongLived(constLongLived), mMutableLongLived(mutableLongLived)
    {}

    // case A: we shouldn't ever be allowed to return a non-const reference to something we only have a const reference to

    // const_cast prevents a useful compiler error
    const LongLived& GetA1() const { return mConstLongLived; }
    LongLived& GetA1()
    {
      return const_cast<LongLived&>( static_cast<const Foo*>(this)->GetA1() );
    }

    /* gives useful compiler error
    LongLived& GetA2()
    {
      return mConstLongLived; // error: invalid initialization of reference of type 'LongLived&' from expression of type 'const LongLived'
    }
    const LongLived& GetA2() const { return const_cast<Foo*>(this)->GetA2(); }
    */

    // case B: imagine we are using the convention that const means thread-safe, and we would prefer to re-calculate than lock the cache, then GetB0 might be correct:

    int GetB0(int i) { return mCache.Nth(i); }
    int GetB0(int i) const { return Fibonachi().Nth(i); }

    /* gives useful compiler error
    int GetB1(int i) const { return mCache.Nth(i); } // error: passing 'const Fibonachi' as 'this' argument of 'int Fibonachi::Nth(int)' discards qualifiers
    int GetB1(int i)
    {
      return static_cast<const Foo*>(this)->GetB1(i);
    }*/

    // const_cast prevents a useful compiler error
    int GetB2(int i) { return mCache.Nth(i); }
    int GetB2(int i) const { return const_cast<Foo*>(this)->GetB2(i); }

    // case C: calling a private const member that returns non-const seems like generally the way to go

    LongLived& GetC1() { return GetC1Private(); }
    const LongLived& GetC1() const { return GetC1Private(); }

  private:
    LongLived& GetC1Private() const { /* pretend a bunch of lines of code instead of just returning a single variable*/ return mMutableLongLived; }

    const LongLived& mConstLongLived;
    LongLived& mMutableLongLived;
    Fibonachi mCache;
};

class Fibonachi
{ 
    public:
      Fibonachi()
      {
        mCache.push_back(0);
        mCache.push_back(1);
      }

      int Nth(int n) 
      {
        for (int i=mCache.size(); i <= n; ++i)
        {
            mCache.push_back(mCache[i-1] + mCache[i-2]);
        }
        return mCache[n];
      }

      int Nth(int n) const
      {
          return n < mCache.size() ? mCache[n] : -1;
      }
    private:
      std::vector<int> mCache;
};

class LongLived {};
JDiMatteo
fonte
Um getter que apenas retorna um membro é menor do que aquele que lança e chama a outra versão de si mesmo. O truque é destinado a funções mais complicadas, nas quais o ganho de desduplicação supera os riscos de conversão.
Sebastian Redl
@SebastianRedl Concordo que a duplicação seria melhor se apenas retornasse um membro. Por favor, imagine que é mais complicado, por exemplo, em vez de retornar mConstLongLived, poderíamos chamar uma função em mConstLongLived que retorne uma referência const, que é usada para chamar outra função que retorne uma referência const que não possuímos e só temos acesso a uma versão const de. Espero que o ponto fique claro que o const_cast pode remover o const de algo que de outra forma não teríamos acesso não-const.
JDiMatteo
4
Isso tudo parece meio ridículo com exemplos simples, mas a duplicação relacionada à const aparece em código real, os erros do compilador const são úteis na prática (geralmente para capturar erros estúpidos), e estou surpreso que a solução "efetiva em C ++" proposta seja estranha e par de elencos aparentemente propensos a erros. Um membro const privado que retorna um não-const parece claramente superior a um elenco duplo, e eu quero saber se há algo que me falta.
JDiMatteo 15/07

Respostas:

8

Ao implementar funções-membro const e não-const que diferem apenas se o ptr / reference retornado é const, a melhor estratégia DRY é:

  1. se estiver escrevendo um acessador, considere se você realmente precisa dele, consulte a resposta do cmaster e http://c2.com/cgi/wiki?AccessorsAreEvil
  2. apenas duplique o código se for trivial (por exemplo, apenas retornando um membro)
  3. nunca use um const_cast para evitar duplicação relacionada ao const
  4. para evitar duplicação não trivial, use uma função const particular retornando um não-const que as funções públicas const e non-const chamam

por exemplo

public:
  LongLived& GetC1() { return GetC1Private(); }
  const LongLived& GetC1() const { return GetC1Private(); }
private:
  LongLived& GetC1Private() const { /* non-trivial DRY logic here */ }

Vamos chamar isso de função const particular, retornando um padrão não const .

Essa é a melhor estratégia para evitar duplicações de maneira direta, enquanto ainda permite ao compilador executar verificações potencialmente úteis e relatar mensagens de erro relacionadas à const.

JDiMatteo
fonte
seus argumentos são bastante convincentes, mas estou um pouco intrigado sobre como você pode obter uma referência não-const a algo de uma constinstância (a menos que a referência seja a algo declarado mutableou a menos que você const_castuse um mas, em ambos os casos, não há probkem para começar) ) Também eu não poderia encontrar qualquer coisa na "função const privada retornando padrão não-const" (se ele tinha a intenção de ser uma piada de chamá-lo padrão .... ele isnt engraçado;)
idclev 463035818
1
Aqui está um exemplo de compilação baseado no código da pergunta: ideone.com/wBE1GB . Desculpe, eu não quis dizer isso como uma piada, mas quis dar um nome aqui (no improvável evento que merece um nome), e atualizei o texto da resposta para tentar deixar isso mais claro. Faz alguns anos desde que escrevi isso, e não me lembro por que pensei que um exemplo passando uma referência no construtor fosse relevante.
JDiMatteo 11/09/19
Obrigado pelo exemplo, não tenho tempo agora, mas definitivamente voltarei a ele. Fwiw aqui é uma resposta que apresenta a mesma abordagem e nos comentários questões semelhantes têm sido apontados: stackoverflow.com/a/124209/4117728
idclev 463035818
1

Sim, você está certo: muitos programas C ++ que tentam corrigir a const estão violando totalmente o princípio DRY, e mesmo o membro privado que retorna a não const é uma complexidade um pouco demais para o conforto.

No entanto, você perde uma observação: a duplicação de código devido à correção da const só é um problema se você estiver dando acesso a outro código aos membros dos dados. Isso por si só viola o encapsulamento. Geralmente, esse tipo de duplicação de código ocorre principalmente em acessadores simples (afinal, você está entregando acesso a membros já existentes, o valor de retorno geralmente não é o resultado de um cálculo).

Minha experiência é que boas abstrações não tendem a incluir acessadores. Consequentemente, evito amplamente esse problema definindo funções de membro que realmente fazem algo, em vez de apenas fornecer acesso aos membros de dados; Eu tento modelar o comportamento em vez de dados. Minha principal intenção é realmente obter alguma abstração de minhas classes e de suas funções-membro individuais, em vez de apenas usar meus objetos como contêineres de dados. Mas esse estilo também é bem-sucedido em evitar toneladas de acessadores de linha única repetitivos const / não-const que são tão comuns na maioria dos códigos.

cmaster - restabelece monica
fonte
Parece estar em debate se os acessadores são bons ou não, por exemplo, consulte a discussão em c2.com/cgi/wiki?AccessorsAreEvil . Na prática, independentemente do que você pensa dos acessadores, grandes bases de código costumam usá-los, e se eles os usarem, seria melhor aderir ao princípio DRY. Então, acho que a pergunta merece mais resposta do que você não deveria perguntar.
JDiMatteo
1
Definitivamente, é uma pergunta que vale a pena fazer :-) E nem vou negar que você precisa de acessadores de tempos em tempos. Estou apenas dizendo que um estilo de programação que não é baseado em acessadores reduz muito o problema. Não resolve o problema completamente, mas é pelo menos bom o suficiente para mim.
cmaster - reinstate monica