¿Cómo elimino la duplicación de código entre funciones miembro const y no const similares?

242

Digamos que tengo lo siguiente class Xdonde quiero devolver el acceso a un miembro 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
    }
};

Las dos funciones miembro X::Z()y X::Z() consttienen un código idéntico dentro de las llaves. Este es un código duplicado y puede causar problemas de mantenimiento para funciones largas con lógica compleja .

¿Hay alguna manera de evitar esta duplicación de código?

Kevin
fuente
En este ejemplo, devolvería un valor en el caso constante para que no pueda refactorizar a continuación. int Z () const {return z; }
Matt Price
1
Para los tipos fundamentales, ¡tienes toda la razón! Mi primer ejemplo no fue muy bueno. Digamos que en su lugar estamos devolviendo alguna instancia de clase. (Actualicé la pregunta para reflejar esto.)
Kevin

Respuestas:

189

Para obtener una explicación detallada, consulte el encabezado "Evitar duplicación consty función no constmiembro", en la pág. 23, en el ítem 3 "Usar constsiempre que sea posible", en Effective C ++ , 3d ed by Scott Meyers, ISBN-13: 9780321334879.

texto alternativo

Aquí está la solución 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;
};

Los dos lanzamientos y la llamada a la función pueden ser feos, pero es correcto. Meyers tiene una explicación detallada de por qué.

jwfearn
fuente
45
Nadie fue despedido por seguir a Scott Meyers :-)
Steve Jessop el
11
witkamp tiene razón en que, en general, es malo usar const_cast. Este es un caso específico donde no lo es, como explica Meyers. @ Adam: ROM => const está bien. const == ROM es obviamente una tontería ya que cualquiera puede lanzar no-const a const willy-nilly: es equivalente a elegir simplemente no modificar algo.
Steve Jessop
44
En general, sugeriría usar const_cast en lugar de static_cast para agregar const, ya que evita que cambie el tipo accidentalmente.
Greg Rogers el
66
@HelloGoodbye: Creo que Meyers asume un mínimo de inteligencia del diseñador de la interfaz de clase. Si get()constdevuelve algo que se definió como un objeto const, entonces no debería haber una versión no const get()en absoluto. En realidad, mi forma de pensar sobre esto ha cambiado con el tiempo: la solución de plantilla es la única forma de evitar la duplicación y obtener una corrección constante del compilador, por lo que personalmente ya no usaría una const_castpara evitar la duplicación de código, elegiría entre poner el código duplicado en una plantilla de función o dejándolo duplicado.
Steve Jessop
77
Las siguientes dos plantillas ayudan enormemente con la legibilidad de esta solución: template<typename T> const T& constant(T& _) { return const_cast<const T&>(_); }y template<typename T> T& variable(const T& _) { return const_cast<T&>(_); }. Entonces puedes hacer:return variable(constant(*this).get());
Casey Rodarmor
64

Sí, es posible evitar la duplicación de código. Debe usar la función miembro const para tener la lógica y hacer que la función miembro no const llame a la función miembro const y vuelva a emitir el valor de retorno a una referencia no const (o puntero si las funciones devuelven un puntero):

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: Es importante que NO ponga la lógica en la función no const y que la función const llame a la función no const, ya que puede dar lugar a un comportamiento indefinido. La razón es que una instancia de clase constante se convierte como una instancia no constante. La función de miembro no constante puede modificar accidentalmente la clase, que según los estados estándar de C ++ dará como resultado un comportamiento indefinido.

Kevin
fuente
3
Wow ... eso es horrible. Simplemente aumentó la cantidad de código, disminuyó la claridad y agregó dos stinkin 'const_cast <> s. ¿Quizás tenga un ejemplo en mente donde esto realmente tiene sentido?
Shog9
14
¡Oye, no digas esto! Puede ser feo, pero según Scott Meyers, es (casi) la forma correcta. Ver efectiva C ++ , 3d ed, punto 3 bajo el encabezamiento "evitar la duplicación de funciones const y no miembros de costo.
jwfearn
17
Si bien entiendo que la solución puede ser fea, imagine que el código que determina qué devolver es de 50 líneas de largo. Entonces la duplicación es altamente indeseable, especialmente cuando tiene que re-factorizar el código. Me he encontrado con esto muchas veces en mi carrera.
Kevin
8
La diferencia entre esto y Meyers es que Meyers tiene static_cast <const X &> (* this). const_cast es para eliminar const, no para agregarlo.
Steve Jessop
8
@VioletGiraffe sabemos que el objeto no se creó originalmente const, ya que es un miembro no constante de un objeto no constante, lo que sabemos porque estamos en un método no constante de dicho objeto. El compilador no hace esta inferencia, sigue una regla conservadora. ¿Por qué crees que existe const_cast, si no fuera por este tipo de situación?
Caleth
47

C ++ 17 ha actualizado la mejor respuesta para esta pregunta:

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

Esto tiene las ventajas de que:

  • Es obvio lo que está pasando
  • Tiene una sobrecarga mínima de código: cabe en una sola línea
  • Es difícil equivocarse (solo puede desecharse volatilepor accidente, pero volatilees un calificador raro)

Si desea ir a la ruta de deducción completa, eso se puede lograr con una función 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;

Ahora ni siquiera puedes equivocarte volatile, y el uso parece

decltype(auto) f() const {
    return something_complicated();
}
decltype(auto) f() {
    return as_mutable(std::as_const(*this).f());
}
David Stone
fuente
Tenga en cuenta que "as_mutable" con la sobrecarga de valor constante eliminada (que generalmente es preferible) impide que el último ejemplo funcione si se f()devuelve en Tlugar de T&.
Max Truxa
1
@MaxTruxa: Sí, y esto es algo bueno. Si solo se compila, tendríamos una referencia pendiente. En el caso de que f()regrese T, no queremos tener dos sobrecargas, constsolo la versión es suficiente.
David Stone
Muy cierto, me disculpo por mi pedo cerebral completo ayer, no tengo idea de lo que estaba pensando cuando escribí ese comentario. Estaba mirando un par getter constante / mutable que devolvía a shared_ptr. Entonces, lo que realmente necesitaba era algo parecido a lo as_mutable_ptrque parece casi idéntico al as_mutableanterior, excepto que toma y devuelve ay shared_ptrusa en std::const_pointer_castlugar de const_cast.
Max Truxa
1
Si un método regresa, T const*entonces esto se uniría en T const* const&&lugar de vincularse a T const* const&(al menos en mi prueba lo hizo). Tuve que agregar una sobrecarga T const*como tipo de argumento para los métodos que devuelven un puntero.
monkey0506
2
@ monkey0506: He actualizado mi respuesta para apoyar los punteros y las referencias
David Stone,
34

Creo que la solución de Scott Meyers se puede mejorar en C ++ 11 mediante el uso de una función auxiliar tempate. Esto hace que la intención sea mucho más obvia y puede reutilizarse para muchos otros captadores.

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 función auxiliar se puede usar de la siguiente manera.

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

El primer argumento es siempre el indicador de este. El segundo es el puntero a la función miembro a llamar. Después de eso, se puede pasar una cantidad arbitraria de argumentos adicionales para que se puedan enviar a la función. Esto necesita C ++ 11 debido a las plantillas variadic.

Pait
fuente
3
Es una pena que no tengamos std::remove_bottom_constque ir con nosotros std::remove_const.
TBBle
No me gusta esta solución porque todavía incluye a const_cast. Puede hacer getElementuna plantilla en sí misma y usar el rasgo del tipo dentro de los mpl::conditionaltipos que necesita, como iteratorso constiterators si es necesario. El verdadero problema es cómo generar una versión constante de un método cuando esta parte de la firma no se puede crear una plantilla.
v.oddou
2
@ v.oddou: std::remove_const<int const&>es int const &(eliminar la constcalificación de nivel superior), de ahí la gimnasia de NonConst<T>esta respuesta. Putativo std::remove_bottom_constpodría eliminar la constcalificación de nivel inferior y hacer precisamente lo que NonConst<T>hace aquí: std::remove_bottom_const<int const&>::type=> int&.
TBBle
44
Esta solución no funciona bien si getElementestá sobrecargada. Entonces el puntero de función no puede resolverse sin dar los parámetros de la plantilla explícitamente. ¿Por qué?
John
1
Debe corregir su respuesta para usar el reenvío perfecto de 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

Un poco más detallado que Meyers, pero podría hacer esto:

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

El método privado tiene la propiedad indeseable de que devuelve un Z & no constante con una instancia constante, por lo que es privado. Los métodos privados pueden romper invariantes de la interfaz externa (en este caso, el invariante deseado es "un objeto constante no puede modificarse a través de referencias obtenidas a través de él a objetos que tiene-a").

Tenga en cuenta que los comentarios son parte del patrón: la interfaz de _getZ especifica que nunca es válido llamarlo (aparte de los accesores, obviamente): no hay ningún beneficio concebible de hacerlo de todos modos, porque es 1 carácter más para escribir y no lo hará resultar en un código más pequeño o más rápido. Llamar al método es equivalente a llamar a uno de los accesos con un const_cast, y tampoco querrás hacerlo. Si le preocupa que los errores sean obvios (y ese es un objetivo justo), llámelo const_cast_getZ en lugar de _getZ.

Por cierto, aprecio la solución de Meyers. No tengo ninguna objeción filosófica al respecto. Sin embargo, personalmente prefiero un poco de repetición controlada y un método privado que solo debe llamarse en ciertas circunstancias estrictamente controladas, en lugar de un método que parece ruido de línea. Elige tu veneno y quédate con él.

[Editar: Kevin ha señalado correctamente que _getZ podría querer llamar a un método adicional (digamos generateZ) que está especializado en constante de la misma manera que getZ. En este caso, _getZ vería un const Z & y tendría que const_cast antes de regresar. Eso sigue siendo seguro, ya que el accesorio repetitivo controla todo, pero no es muy obvio que sea seguro. Además, si haces eso y luego cambias generateZ para que siempre devuelva const, entonces también debes cambiar getZ para que siempre devuelva const, pero el compilador no te dirá que lo haces.

Este último punto sobre el compilador también es cierto para el patrón recomendado por Meyers, pero el primer punto sobre un const_cast no obvio no lo es. A fin de cuentas, creo que si _getZ necesita un const_cast para su valor de retorno, entonces este patrón pierde mucho de su valor sobre el de Meyers. Como también sufre desventajas en comparación con las de Meyers, creo que cambiaría a las suyas en esa situación. Refactorizar de uno a otro es fácil: no afecta a ningún otro código válido en la clase, ya que solo el código no válido y el repetitivo llaman a _getZ.]

Steve Jessop
fuente
3
Esto todavía tiene el problema de que lo que devuelve puede ser constante para una instancia constante de X. En ese caso, aún necesita un const_cast en _getZ (...). Si los desarrolladores posteriores lo utilizan incorrectamente, aún puede conducir a UB. Si lo que se devuelve es 'mutable', entonces esta es una buena solución.
Kevin
1
Cualquier función privada (diablos, públicas también) puede ser mal utilizada por los desarrolladores posteriores, si eligen ignorar las instrucciones de BLOCK CAPITAL sobre su uso válido, en el archivo de encabezado y también en Doxygen, etc. No puedo detener eso, y no lo considero mi problema ya que las instrucciones son fáciles de entender.
Steve Jessop
13
-1: Esto no funciona en muchas situaciones. ¿Qué pasa si somethingen la _getZ()función hay una variable de instancia? El compilador (o al menos algunos compiladores) se quejará de que dado que _getZ()es const, cualquier variable de instancia a la que se haga referencia también es const. Entonces somethingsería const (sería de tipo const Z&) y no se podría convertir a Z&. En mi experiencia (ciertamente algo limitada), la mayoría de las veces somethinges una variable de instancia en casos como este.
Gravity
2
@GravityBringer: entonces "algo" debe involucrar a const_cast. Estaba destinado a ser un marcador de posición para el código requerido para obtener un retorno no constante del objeto const, no como un marcador de posición para lo que habría estado en el getter duplicado. Entonces "algo" no es solo una variable de instancia.
Steve Jessop
2
Veo. Sin embargo, eso realmente disminuye la utilidad de la técnica. Quitaría el voto negativo, pero SO no me deja.
Gravity
22

Buena pregunta y buenas respuestas. Tengo otra solución, que no 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);
    }

};

Sin embargo, tiene la fealdad de requerir un miembro estático y la necesidad de usar la instancevariable dentro de él.

No consideré todas las posibles implicaciones (negativas) de esta solución. Por favor avíseme si alguno.

gd1
fuente
44
Bueno, vamos con el simple hecho de que agregaste más repetitivo. En todo caso, esto debería usarse como un ejemplo de por qué el lenguaje necesita una forma de modificar los calificadores de función junto con el tipo de retorno auto get(std::size_t i) -> auto(const), auto(&&). Por qué '&&'? Ahh, así que puedo decir:auto foo() -> auto(const), auto(&&) = delete;
kfsone
gd1: exactamente lo que tenía en mente. @kfsone y exactamente lo que concluí también.
v.oddou
1
@kfsone la sintaxis debería incorporar thispalabras clave. Sugiero que template< typename T > auto myfunction(T this, t args) -> decltype(ident)esta palabra clave se reconozca como el argumento de instancia de objeto implícito y permita que el compilador reconozca que myfunction es un miembro o T. Tse deducirá automáticamente en el sitio de la llamada, que siempre será el tipo de la clase, pero con calificación gratuita de cv.
v.oddou
2
Esa solución también tiene la ventaja (versus la const_cast) para permitir el retorno iteratory const_iterator.
Jarod42
1
Si la implementación se mueve en un archivo cpp (y como el método no duplicado no debería ser trivial, probablemente sea el caso), staticse puede hacer en el ámbito del archivo en lugar del ámbito de la clase. :-)
Jarod42
8

También podría resolver esto con plantillas. Esta solución es un poco fea (pero la fealdad está oculta en el archivo .cpp) pero proporciona una comprobación del compilador de la constancia y no hay duplicación de código.

archivo .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;
};

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

La principal desventaja que puedo ver es que debido a que toda la implementación compleja del método se encuentra en una función global, es necesario que se apoyen de los miembros de X utilizando métodos públicos como GetVector () arriba (de los cuales siempre debe haber un versión const y no const) o puede hacer que esta función sea un amigo. Pero no me gustan los amigos.

[Editar: se eliminó la inclusión innecesaria de cstdio agregado durante la prueba.]

Andy Balaam
fuente
3
Siempre puede hacer que la función de implementación compleja sea un miembro estático para obtener acceso a los miembros privados. La función solo necesita declararse en el archivo de encabezado de clase, la definición puede residir en el archivo de implementación de clase. Después de todo, es parte de la implementación de la clase.
CB Bailey
Aah si buena idea! No me gusta que las plantillas aparezcan en el encabezado, pero si desde aquí potencialmente hace que la implementación sea mucho más simple, probablemente valga la pena.
Andy Balaam
+ 1 a esta solución que no duplica ningún código, ni utiliza ninguno feo const_cast(que podría usarse accidentalmente para enlazar algo que en realidad se supone que es compatible con algo que no lo es).
Hola
Hoy en día esto se puede simplificar con un tipo de retorno deducido para la plantilla (especialmente útil, ya que reduce lo que debe duplicarse en la clase en el caso miembro).
Davis Herring
3

¿Qué hay de mover la lógica a un método privado y solo hacer las cosas de "obtener la referencia y devolver" dentro de los captadores? En realidad, estaría bastante confundido acerca de los lanzamientos estáticos y constantes dentro de una función getter simple, ¡y lo consideraría feo, excepto por circunstancias extremadamente raras!

MP24
fuente
Para evitar un comportamiento indefinido, aún necesita un const_cast. Vea la respuesta de Martin York y mi comentario allí.
Kevin
1
Kevin, qué respuesta por Martin York
Peter Nimmo
2

¿Es una trampa usar el preprocesador?

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

};

No es tan elegante como las plantillas o los moldes, pero hace que su intención ("estas dos funciones sean idénticas") sea bastante explícita.

usuario1476176
fuente
1
Pero luego debe tener cuidado con las barras invertidas (como es habitual en las macros multilínea) y, además, pierde el resaltado de sintaxis en la mayoría de los editores (si no en todos).
Ruslan
2

Me sorprende que haya tantas respuestas diferentes, pero casi todas se basan en una gran plantilla de magia. Las plantillas son poderosas, pero a veces las macros las superan con concisión. La máxima versatilidad a menudo se logra combinando ambos.

Escribí una macro FROM_CONST_OVERLOAD()que se puede colocar en la función no const para invocar la función const.

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

Implementación simple y reutilizable:

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)

Explicación:

Como se publica en muchas respuestas, el patrón típico para evitar la duplicación de código en una función miembro no constante es este:

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

Se puede evitar mucho de este repetitivo usando inferencia de tipos. Primero, const_castse puede encapsular en WithoutConst(), lo que infiere el tipo de su argumento y elimina el calificador const. En segundo lugar, se puede utilizar un enfoque similar WithConst()para calificar constantemente el thispuntero, lo que permite llamar al método const-overloaded.

El resto es una macro simple que antepone la llamada con la calificación correcta this->y elimina const del resultado. Dado que la expresión utilizada en la macro es casi siempre una simple llamada a función con argumentos reenviados 1: 1, los inconvenientes de las macros, como la evaluación múltiple, no entran en acción. Los puntos suspensivos y __VA_ARGS__también podrían usarse, pero no deberían ser necesarios porque las comas (como los separadores de argumentos) aparecen entre paréntesis.

Este enfoque tiene varios beneficios:

  • Sintaxis mínima y natural: simplemente envuelva la llamada FROM_CONST_OVERLOAD( )
  • No se requiere función de miembro adicional
  • Compatible con C ++ 98
  • Implementación simple, sin metaprogramación de plantilla y cero dependencias
  • Extensibles: const otras relaciones pueden ser añadidos (como const_iterator, std::shared_ptr<const T>, etc.). Para esto, simplemente sobrecargue WithoutConst()los tipos correspondientes.

Limitaciones: esta solución está optimizada para escenarios en los que la sobrecarga no constante está haciendo exactamente lo mismo que la sobrecarga constante, de modo que los argumentos pueden reenviarse 1: 1. Si su lógica es diferente y no está llamando a la versión constante a través de this->Method(args), puede considerar otros enfoques.

El operador
fuente
2

Para aquellos (como yo) que

  • usar c ++ 17
  • desea agregar la menor cantidad de repeticiones / repeticiones y
  • no importa usar macros (mientras espera metaclases ...),

Aquí hay otra toma:

#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)...));              \
    }

Básicamente es una combinación de las respuestas de @Pait, @DavidStone y @ sh1 ( EDITAR : y una mejora de @cdhowie). Lo que agrega a la tabla es que se saldrá con una sola línea de código adicional que simplemente nombra la función (pero sin argumento o duplicación de tipo de retorno):

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

Nota: gcc no puede compilar esto antes de 8.1, clang-5 y hacia arriba, así como MSVC-19 están contentos (según el explorador del compilador ).

axxel
fuente
Esto me funcionó directamente. Esta es una gran respuesta, gracias!
Corto
¿No deberían decltype()usarse los s también std::forwarden los argumentos para asegurarse de que estamos usando el tipo de retorno correcto en el caso de que tengamos sobrecargas get()que tomen diferentes tipos de referencias?
cdhowie
@cdhowie ¿Puedes dar un ejemplo?
axxel
@axxel Está ideado como el infierno, pero aquí tienes . La NON_CONSTmacro deduce el tipo de retorno incorrectamente y const_casts al tipo incorrecto debido a la falta de reenvío en los decltype(func(a...))tipos. Reemplazarlos con decltype(func(std::forward<T>(a)...)) resuelve esto . (Solo hay un error de vinculador porque nunca X::get
definí
1
Gracias @cdhowie, prodigé tu ejemplo para usar realmente las sobrecargas no constantes: coliru.stacked-crooked.com/a/0cedc7f4e789479e
axxel
1

Aquí hay una versión C ++ 17 de la función auxiliar de plantilla estática, con una prueba 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); }
};

Versión completa: https://godbolt.org/z/mMK4r3

atablash
fuente
1

Se me ocurrió una macro que genera pares de funciones const / non-const automáticamente.

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

Vea el final de la respuesta para la implementación.

El argumento de MAYBE_CONSTestá duplicado. En la primera copia, CVse reemplaza por nada; y en la segunda copia se reemplaza con const.

No hay límite en cuántas veces CVpuede aparecer en el argumento macro.

Sin embargo, hay un pequeño inconveniente. Si CVaparece dentro de paréntesis, este par de paréntesis debe tener el prefijo 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 &); )

Implementación:

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

Implementación anterior a C ++ 20 que no admite 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
fuente
0

Por lo general, las funciones miembro para las que necesita versiones const y no const son getters y setters. La mayoría de las veces son de una sola línea, por lo que la duplicación de código no es un problema.

Dima
fuente
2
Eso puede ser cierto la mayor parte del tiempo. Pero hay excepciones.
Kevin
1
getters de todos modos, un const setter no tiene mucho sentido;)
jwfearn
Quise decir que el getter no constante es efectivamente un setter. :)
Dima
0

Hice esto para un amigo que justificó legítimamente el uso de const_cast... sin saberlo, probablemente habría hecho algo como esto (no muy 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
fuente
0

Sugeriría una plantilla de función estática de ayuda privada, como esta:

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
fuente
-1

Este artículo de DDJ muestra una forma de usar la especialización de plantilla que no requiere que uses const_cast. Sin embargo, para una función tan simple realmente no es necesaria.

boost :: any_cast (en un punto, ya no lo hace) usa un const_cast de la versión const que llama a la versión no const para evitar la duplicación. Sin embargo, no puede imponer semántica constante en la versión no constante, por lo que debe tener mucho cuidado con eso.

Al final, una duplicación de código está bien siempre que los dos fragmentos estén directamente uno encima del otro.

Greg Rogers
fuente
El artículo de DDJ parece referirse a iteradores, lo que no es relevante para la pregunta. Los iteradores constantes no son datos constantes: son iteradores que apuntan a datos constantes.
Kevin
-1

Para agregar a la solución proporcionada por jwfearn y kevin, aquí está la solución correspondiente cuando la función devuelve 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
fuente
-1

No encontré lo que estaba buscando, así que rodé un par de los míos ...

Este es un poco prolijo, pero tiene la ventaja de manejar muchos métodos sobrecargados del mismo nombre (y tipo de retorno) a la 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...));
  }
};

Si solo tiene un constmétodo por nombre, pero todavía hay muchos métodos para duplicar, entonces puede preferir esto:

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

Desafortunadamente, esto se rompe tan pronto como comienza a sobrecargar el nombre (la lista de argumentos del argumento del puntero de la función parece no estar resuelta en ese punto, por lo que no puede encontrar una coincidencia para el argumento de la función). Aunque también puedes crear una plantilla para salir de eso:

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

Pero los argumentos de referencia al constmétodo no coinciden con los argumentos aparentemente por valor de la plantilla y se rompe. No estoy seguro de por qué. He aquí por qué .

sh1
fuente