Como removo a duplicação de código entre funções-membro const e não-const similares?

242

Digamos que possuo o seguinte, class Xonde 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() consttê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?

Kevin
fonte
Neste exemplo, eu retornaria um valor no caso const, para que você não possa refatorar abaixo. int Z () const {retorno z; }
Matt Price
1
Para tipos fundamentais, você está absolutamente correto! Meu primeiro exemplo não foi muito bom. Digamos que, em vez disso, estamos retornando alguma instância de classe. (Eu atualizei a questão para refletir isso.)
Kevin

Respostas:

189

Para uma explicação detalhada, consulte o título "Evitar duplicação de funções conste não pertencentes a constmembros", na p. 23, no item 3 "Use constsempre que possível", em C ++ efetivo , 3d ed por Scott Meyers, ISBN-13: 9780321334879.

texto alternativo

Aqui está a solução de Meyers (simplificada):

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

As duas transmissões e chamada de função podem ser feias, mas estão corretas. Meyers tem uma explicação completa do porquê.

jwfearn
fonte
45
Ninguém nunca foi demitido por seguir Scott Meyers :-)
Steve Jessop
11
O witkamp está correto, de modo geral que é ruim usar const_cast. Este é um caso específico em que não é, como explica Meyers. @ Adam: ROM => const está bem. const == ROM é obviamente um absurdo, já que qualquer pessoa pode converter non-const em const-willy-nilly: é equivalente a apenas optar por não modificar algo.
Steve Jessop
44
Em geral, eu sugiro usar const_cast em vez de static_cast para adicionar const, pois isso impede que você altere o tipo acidentalmente.
Greg Rogers
6
@HelloGoodbye: Acho que Meyers assume um pouco de inteligência do designer da interface da classe. Se get()constretornar algo que foi definido como um objeto const, então não deve haver uma versão não-const get(). 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 um const_castpara evitar a duplicação de código, eu escolheria colocar o código enganado em um modelo de função ou deixando-o enganado.
precisa
7
Os dois modelos a seguir ajudam muito na legibilidade dessa solução: template<typename T> const T& constant(T& _) { return const_cast<const T&>(_); }e template<typename T> T& variable(const T& _) { return const_cast<T&>(_); }. Então você pode fazer:return variable(constant(*this).get());
Casey Rodarmor
64

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):

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ê 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.

Kevin
fonte
3
Uau ... isso é horrível. Você acabou de aumentar a quantidade de código, diminuiu a clareza e adicionou dois stinkin 'const_cast <> s. Talvez você tenha um exemplo em mente onde isso realmente faz sentido?
Shog9
14
Ei, não brinque com isso !, pode ser feio, mas, de acordo com Scott Meyers, é (quase) o caminho correto. Consulte C ++ efetivo , 3d ed, Item 3, sob o título "Evitando duplicação nas funções membro const e sem custo."
jwfearn
17
Embora eu entenda que a solução pode ser feia, imagine que o código que determina o que retornar tem 50 linhas. Então a duplicação é altamente indesejável - especialmente quando você precisa refazer o código. Eu encontrei isso muitas vezes na minha carreira.
Kevin
8
A diferença entre isso e Meyers é que Meyers possui static_cast <const X &> (* this). const_cast é para remover const, não adicioná-lo.
Steve Jessop
8
@VioletGiraffe, sabemos que o objeto não foi originalmente criado const, pois é um membro não const de um objeto não const, o que sabemos porque estamos em um método não const do objeto. O compilador não faz essa inferência, segue uma regra conservadora. Por que você acha que const_cast existe, se não para esse tipo de situação?
Caleth
47

O C ++ 17 atualizou a melhor resposta para esta pergunta:

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

Isso tem as vantagens de que:

  • É óbvio o que está acontecendo
  • Possui sobrecarga mínima de código - cabe em uma única linha
  • É difícil errar (só pode ser descartado volatilepor acidente, mas volatileé um classificador raro)

Se você deseja seguir o caminho completo da dedução, isso pode ser realizado com uma função auxiliar

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

Agora você não pode nem errar volatile, e o uso parece

decltype(auto) f() const {
    return something_complicated();
}
decltype(auto) f() {
    return as_mutable(std::as_const(*this).f());
}
David Stone
fonte
Observe que "as_mutable" com a sobrecarga de valor constante excluída (o que geralmente é preferível) impede que o último exemplo funcione se f()retornar em Tvez de T&.
Max Truxa
1
@ MaxTruxa: Sim, e isso é uma coisa boa. Se apenas compilasse, teríamos uma referência pendente. No caso em que f()retorna T, não queremos ter duas sobrecargas, apenas a constversão é suficiente.
David Stone
É verdade que peço desculpas pelo peido cheio de meu cérebro ontem, não faço ideia do que estava pensando quando escrevi esse comentário. Eu estava olhando para um par de constantes / mutáveis ​​getter retornando a shared_ptr. Então, o que eu realmente precisava era de algo parecido as_mutable_ptrcom o as_mutableacima, exceto que ele pega e retorna ae shared_ptrusa em std::const_pointer_castvez de const_cast.
Max Truxa
1
Se um método retornar T const*, isso vincularia ao T const* const&&invés de vincular T const* const&(pelo menos nos meus testes). Eu tive que adicionar uma sobrecarga para T const*como o tipo de argumento para métodos retornando um ponteiro.
monkey0506
2
@ monkey0506: Eu atualizei a minha resposta para ponteiros de apoio, bem como referências
David Stone
34

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.

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 usada 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 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.

Pait
fonte
3
É uma pena que não tenhamos std::remove_bottom_constque concordar std::remove_const.
precisa saber é o seguinte
Não gosto desta solução porque ainda incorpora a const_cast. Você pode criar getElementum modelo em si e usar a característica do tipo dentro dos mpl::conditionaltipos que você precisa, como iterators ou constiterators, 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?
v.oddou
2
@ v.oddou: std::remove_const<int const&>is int const &(remova a constqualificação de nível superior ), daí a ginástica NonConst<T>desta resposta. O putativo std::remove_bottom_constpode remover a constqualificação de nível inferior e fazer exatamente o que NonConst<T>faz aqui: std::remove_bottom_const<int const&>::type=> int&.
precisa saber é o seguinte
4
Esta solução não funciona bem se getElementestiver sobrecarregada. Em seguida, o ponteiro da função não pode ser resolvido sem fornecer explicitamente os parâmetros do modelo. Por quê?
John
1
Você precisa corrigir a resposta para usar o encaminhamento perfeito do C ++ 11: 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/bca26a8590265492e2f2760d3cefcf83
ShaulF:
22

Um pouco mais detalhado do que Meyers, mas eu posso 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 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.]

Steve Jessop
fonte
3
Isso ainda tem o problema de que o que você devolve pode ser constante para uma instância constante do X. Nesse caso, você ainda precisa de um const_cast em _getZ (...). Se mal utilizado por desenvolvedores posteriores, ainda pode levar ao UB. Se o que está sendo retornado for 'mutável', essa é uma boa solução.
Kevin
1
Qualquer função privada (diabos, públicas também) pode ser mal utilizada por desenvolvedores posteriores, se optarem por ignorar as instruções do BLOCK CAPITAL em seu uso válido, no arquivo de cabeçalho e também no Doxygen etc. Não consigo parar com isso, e não considero meu problema, pois as instruções são fáceis de entender.
Steve Jessop
13
-1: isso não funciona em muitas situações. E se somethingna _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, somethingseria const (seria do tipo const Z&) e não poderia ser convertido em Z&. Na minha experiência (reconhecidamente um pouco limitada), na maioria das vezes somethingé uma variável de instância em casos como este.
Gravity
2
@ GravityBringer: então "algo" precisa envolver a 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.
precisa
2
Entendo. Isso realmente diminui a utilidade da técnica, no entanto. Eu removeria o voto negativo, mas o SO não vai me deixar.
Gravidade
22

Boa pergunta e boas respostas. Eu tenho 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, possui a feiúra de exigir um membro estático e a necessidade de usar a instancevariável dentro dele.

Não considerei todas as implicações possíveis (negativas) dessa solução. Por favor me avise se houver.

gd1
fonte
4
Bem, vamos ao simples fato de você ter adicionado mais clichês. Se alguma coisa, isso deve ser usado como um exemplo de por que o idioma precisa de uma maneira de modificar os qualificadores de função junto com o tipo de retorno auto get(std::size_t i) -> auto(const), auto(&&). Por quê '&&'? Ahh, então posso dizer:auto foo() -> auto(const), auto(&&) = delete;
kfsone
gd1: exatamente o que eu tinha em mente. @kfsone e exatamente o que eu concluí também.
precisa saber é
1
@kfsone a sintaxe deve incorporar thispalavras-chave. Eu sugiro que template< 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 ou T. Tserá deduzido automaticamente no site da chamada, que sempre será o tipo de turma, mas com qualificação cv gratuita.
precisa saber é
2
Essa solução também tem a vantagem (versus a const_cast) de permitir retornar iteratore const_iterator.
Jarod42
1
Se a implementação for movida no arquivo cpp (e como o método para não duplicar não deve ser trivial, provavelmente seria o caso), isso staticpode ser feito no escopo do arquivo, em vez do escopo da classe. :-)
Jarod42
8

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:

#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, 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.]

Andy Balaam
fonte
3
Você sempre pode tornar a função de implementação complexa um membro estático para obter acesso aos membros privados. A função precisa ser declarada apenas no arquivo de cabeçalho da classe, a definição pode residir no arquivo de implementação da classe. É, afinal, parte da implementação da classe.
CB Bailey
Aah sim boa ideia! Eu não gosto do material do modelo que aparece no cabeçalho, mas se, desde aqui, potencialmente torna a implementação muito mais simples, provavelmente vale a pena.
Andy Balaam
+ 1 para esta solução que não duplica nenhum código, nem usa feio const_cast(que pode ser acidentalmente usado para separar algo que na verdade deveria ser const ou algo que não é).
hellogoodbye
Atualmente, isso pode ser simplificado com um tipo de retorno deduzido para o modelo (especialmente útil, pois reduz o que deve ser duplicado na classe no caso de membro).
Davis Herring
3

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!

MP24
fonte
Para evitar um comportamento indefinido, você ainda precisa de um const_cast. Veja a resposta de Martin York e meu comentário lá.
Kevin
1
Kevin, que resposta Martin York
Peter Nimmo
2

É trapaça 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 sofisticado quanto modelos ou elencos, mas torna sua intenção ("essas duas funções devem ser idênticas") bastante explícita.

user1476176
fonte
1
Mas é preciso ter cuidado com as barras invertidas (como de costume para macros com várias linhas) e, além disso, você perde o destaque da sintaxe na maioria dos editores (se não todos).
Ruslan
2

É 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:

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) );
    }
};

Implementação simples e 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:

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 é:

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

Muito deste padrão pode ser evitado usando inferência de tipo. Primeiro, const_castpode ser encapsulado em WithoutConst(), que infere o tipo de seu argumento e remove o const-qualifier. Segundo, uma abordagem semelhante pode ser usada WithConst()para qualificar constantemente o thisponteiro, 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:

  • Sintaxe mínima e natural - basta encerrar a chamada FROM_CONST_OVERLOAD( )
  • Nenhuma função de membro extra é necessária
  • Compatível com C ++ 98
  • Implementação simples, sem metaprogramação de modelo e zero dependências
  • Extensíveis: outras relações const pode ser adicionado (como const_iterator, std::shared_ptr<const T>, etc.). Para isso, basta sobrecarregar WithoutConst()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.

O operador
fonte
2

Para aqueles (como eu) que

  • use c ++ 17
  • deseja adicionar a menor quantidade de clichê / repetição e
  • não se importe em usar macros (enquanto aguarda as meta-classes ...),

aqui está outra tomada:

#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(std::forward<T>(a)...))>::type   \
    {                                                                       \
        return const_cast<decltype(func(std::forward<T>(a)...))>(           \
            std::as_const(*this).func(std::forward<T>(a)...));              \
    }

É 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):

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

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 ).

axxel
fonte
Isso só funcionou direto para mim. Esta é uma ótima resposta, obrigado!
Curto
Os decltype()s também não deveriam estar usando std::forwardos argumentos para garantir que estamos usando o tipo de retorno correto no caso em que temos sobrecargas get()que requerem diferentes tipos de referência?
cdhowie
@cdhowie Você pode dar um exemplo?
axxel
@axxel É artificial como o inferno, mas aqui está . A NON_CONSTmacro deduz incorretamente o tipo de retorno const_castes no tipo incorreto devido à falta de encaminhamento nos decltype(func(a...))tipos. Substituí-los por decltype(func(std::forward<T>(a)...)) resolve isso . (Há apenas um erro de vinculador porque eu nunca definiu qualquer um dos declarados X::getsobrecargas.)
cdhowie
1
Obrigado @cdhowie, mostrei seu exemplo para realmente usar as sobrecargas não const: coliru.stacked-crooked.com/a/0cedc7f4e789479e
axxel
1

Aqui está uma versão C ++ 17 da função auxiliar estática do modelo, com um teste SFINAE opcional.

#include <type_traits>

#define REQUIRES(...)         class = std::enable_if_t<(__VA_ARGS__)>
#define REQUIRES_CV_OF(A,B)   REQUIRES( std::is_same_v< std::remove_cv_t< A >, B > )

class Foobar {
private:
    int something;

    template<class FOOBAR, REQUIRES_CV_OF(FOOBAR, Foobar)>
    static auto& _getSomething(FOOBAR& self, int index) {
        // big, non-trivial chunk of code...
        return self.something;
    }

public:
    auto& getSomething(int index)       { return _getSomething(*this, index); }
    auto& getSomething(int index) const { return _getSomething(*this, index); }
};

Versão completa: https://godbolt.org/z/mMK4r3

atablash
fonte
1

Eu vim com uma macro que gera pares de funções const / non-const automaticamente.

class A
{
    int x;    
  public:
    MAYBE_CONST(
        CV int &GetX() CV {return x;}
        CV int &GetY() CV {return y;}
    )

    //   Equivalent to:
    // int &GetX() {return x;}
    // int &GetY() {return y;}
    // const int &GetX() const {return x;}
    // const int &GetY() const {return y;}
};

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 CVaparecer entre parênteses, esse par de parênteses deve ser prefixado com CV_IN:

// Doesn't work
MAYBE_CONST( CV int &foo(CV int &); )

// Works, expands to
//         int &foo(      int &);
//   const int &foo(const int &);
MAYBE_CONST( CV int &foo CV_IN(CV int &); )

Implementação:

#define MAYBE_CONST(...) IMPL_CV_maybe_const( (IMPL_CV_null,__VA_ARGS__)() )
#define CV )(IMPL_CV_identity,
#define CV_IN(...) )(IMPL_CV_p_open,)(IMPL_CV_null,__VA_ARGS__)(IMPL_CV_p_close,)(IMPL_CV_null,

#define IMPL_CV_null(...)
#define IMPL_CV_identity(...) __VA_ARGS__
#define IMPL_CV_p_open(...) (
#define IMPL_CV_p_close(...) )

#define IMPL_CV_maybe_const(seq) IMPL_CV_a seq IMPL_CV_const_a seq

#define IMPL_CV_body(cv, m, ...) m(cv) __VA_ARGS__

#define IMPL_CV_a(...) __VA_OPT__(IMPL_CV_body(,__VA_ARGS__) IMPL_CV_b)
#define IMPL_CV_b(...) __VA_OPT__(IMPL_CV_body(,__VA_ARGS__) IMPL_CV_a)

#define IMPL_CV_const_a(...) __VA_OPT__(IMPL_CV_body(const,__VA_ARGS__) IMPL_CV_const_b)
#define IMPL_CV_const_b(...) __VA_OPT__(IMPL_CV_body(const,__VA_ARGS__) IMPL_CV_const_a)

Implementação pré-C ++ 20 que não suporta CV_IN:

#define MAYBE_CONST(...) IMPL_MC( ((__VA_ARGS__)) )
#define CV ))((

#define IMPL_MC(seq) \
    IMPL_MC_end(IMPL_MC_a seq) \
    IMPL_MC_end(IMPL_MC_const_0 seq)

#define IMPL_MC_identity(...) __VA_ARGS__
#define IMPL_MC_end(...) IMPL_MC_end_(__VA_ARGS__)
#define IMPL_MC_end_(...) __VA_ARGS__##_end

#define IMPL_MC_a(elem) IMPL_MC_identity elem IMPL_MC_b
#define IMPL_MC_b(elem) IMPL_MC_identity elem IMPL_MC_a
#define IMPL_MC_a_end
#define IMPL_MC_b_end

#define IMPL_MC_const_0(elem)       IMPL_MC_identity elem IMPL_MC_const_a
#define IMPL_MC_const_a(elem) const IMPL_MC_identity elem IMPL_MC_const_b
#define IMPL_MC_const_b(elem) const IMPL_MC_identity elem IMPL_MC_const_a
#define IMPL_MC_const_a_end
#define IMPL_MC_const_b_end
HolyBlackCat
fonte
0

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.

Dima
fonte
2
Isso pode ser verdade na maioria das vezes. Mas há exceções.
Kevin
1
getters de qualquer maneira, um setter const não faz muito sentido;)
jwfearn
Eu quis dizer que o non-const getter é efetivamente um setter. :)
Dima
0

Eu fiz isso por um amigo que justificou o uso de const_cast... sem saber, provavelmente teria feito algo assim (não muito 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;
}
matovitch
fonte
0

Eu sugeriria um modelo de função estática de auxiliar particular, assim:

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);
    }
};
dats
fonte
-1

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.

Greg Rogers
fonte
O artigo do DDJ parece se referir a iteradores - o que não é relevante para a questão. Const-iteradores não são dados constantes - são iteradores que apontam para dados constantes.
Kevin
-1

Para adicionar à solução fornecida pelo jwfearn e pelo kevin, 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;
};
Christer Swahn
fonte
-1

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:

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ê tiver apenas um constmétodo por nome, mas ainda houver muitos métodos para duplicar, poderá preferir o seguinte:

  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 é 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:

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

Mas os argumentos de referência para o constmé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ê .

sh1
fonte