Este equipo de especialistas pasados algunos días de trabajo y de recopilar de datos, hemos dado con los datos necesarios, deseamos que te sea de gran utilidad en tu plan.
Solución:
Hay dos formas distintas de “propiedades” que aparecen en la biblioteca estándar, que categorizaré como “orientadas a la identidad” y “orientadas al valor”. Lo que elija depende de cómo debe interactuar el sistema con Foo
. Tampoco es “más correcto”.
Orientado a la identidad
class Foo
X x_;
public:
X & x() return x_;
const X & x() const return x_;
Aquí devolvemos un referencia al subyacente X
miembro, que permite a ambos lados del sitio de la llamada observar los cambios iniciados por el otro. los X
El miembro es visible para el mundo exterior, presumiblemente porque su identidad es importante. A primera vista, puede parecer que solo existe el lado “obtener” de una propiedad, pero este no es el caso si X
es asignable.
Foo f;
f.x() = X ... ;
Orientado al valor
class Foo
X x_;
public:
X x() const return x_;
void x(X x) x_ = std::move(x);
Aquí devolvemos un Copiar de El X
miembro, y acepta un Copiar para sobrescribir. Los cambios posteriores en ambos lados no se propagan. Presumiblemente solo nos preocupamos por el valor de x
en este caso.
A lo largo de los años, he llegado a creer que toda la noción de getter / setter suele ser un error. Por muy contrario que pueda parecer, una variable pública es normalmente la respuesta correcta.
El truco es que la variable pública debe ser del tipo correcto. En la pregunta, especificó que hemos escrito un establecedor que verifica el valor que se está escribiendo, o que solo estamos escribiendo un getter (por lo que tenemos un const
objeto).
Yo diría que ambos básicamente están diciendo algo como: “X es un int. Solo que no es realmente un int, es algo así como un int, pero con estas restricciones adicionales …”
Y eso nos lleva al punto real: si una mirada cuidadosa a X muestra que realmente es un tipo diferente, entonces defina el tipo que realmente es y luego créelo como un miembro público de ese tipo. Lo básico de esto podría verse así:
template
class checked
T value;
std::function check;
public:
template
checked(checker check)
: check(check)
, value(check(T()))
checked &operator=(T const &in) value = check(in); return *this;
operator T() const return value;
friend std::ostream &operator<<(std::ostream &os, checked const &c)
return os << c.value;
friend std::istream &operator>>(std::istream &is, checked &c)
try
T input;
is >> input;
c = input;
catch (...)
is.setstate(std::ios::failbit);
return is;
;
Esto es genérico, por lo que el usuario puede especificar algo similar a una función (p. Ej., Una lambda) que asegure que el valor es correcto; podría pasar el valor sin cambios, o podría modificarlo (p. Ej., Para un tipo de saturación) o podría lanzar una excepción, pero si no lo hace, lo que devuelve debe ser un valor aceptable para el tipo que se especifica.
Entonces, por ejemplo, para obtener un tipo entero que solo permite valores de 0 a 10 y se satura en 0 y 10 (es decir, cualquier número negativo se convierte en 0, y cualquier número mayor que 10 se convierte en 10, podríamos escribir código en este general pedido:
checked foo([](auto i) return std::min(std::max(i, 0), 10); );
Entonces podemos hacer más o menos las cosas habituales con un foo
, con la seguridad de que siempre estará en el rango 0..10:
std::cout << "Please enter a number from 0 to 10: ";
std::cin >> foo; // inputs will be clamped to range
std::cout << "You might have entered: " << foo << "n";
foo = foo - 20; // result will be clamped to range
std::cout << "After subtracting 20: " << foo;
Con esto, podemos hacer público el miembro de forma segura, porque el tipo que hemos definido es realmente el tipo que queremos que sea; las condiciones que queremos colocar en él son inherentes al tipo, no algo agregado después del hecho (por así decirlo) por el getter / setter.
Por supuesto, ese es el caso en el que queremos restringir los valores de alguna manera. Si solo queremos un tipo que sea efectivamente de solo lectura, es mucho más fácil: solo una plantilla que define un constructor y un operator T
, pero no un operador de asignación que toma una T como parámetro.
Por supuesto, algunos casos de entrada restringida pueden ser más complejos. En algunos casos, desea algo así como una relación entre dos cosas, entonces (por ejemplo) foo
debe estar en el rango 0..1000, y bar
debe estar entre 2x y 3x foo
. Hay dos formas de manejar este tipo de cosas. Una es usar la misma plantilla que la anterior, pero con el tipo subyacente siendo un std::tuple
y ve desde allí. Si sus relaciones son realmente complejas, puede terminar queriendo definir una clase separada por completo para definir los objetos en esa relación compleja.
Resumen
Defina que su miembro sea del tipo que realmente desea, y todas las cosas útiles que el captador / definidor podría / haría quedar incluidas en las propiedades de ese tipo.
Así es como escribiría un setter / getter genérico:
class Foo
private:
X x_;
public:
auto x() -> X& return x_;
auto x() const -> const X& return x_;
;
Intentaré explicar el razonamiento detrás de cada transformación:
El primer problema con su versión es que en lugar de pasar valores, debe pasar referencias constantes. Esto evita la copia innecesaria. Cierto, ya que C++11
el valor se puede mover, pero eso no siempre es posible. Para tipos de datos básicos (p. Ej. int
) usar valores en lugar de referencias está bien.
Así que primero corregimos eso.
class Foo1
private:
X x_;
public:
void set_x(const X& value)
// ^~~~~ ^
x_ = value;
const X& get_x()
// ^~~~~ ^
return x_;
;
Todavía hay un problema con la solución anterior. Ya que get_x
no modifica el objeto debe ser marcado const
. Esto es parte de un principio de C ++ llamado const corrección.
La solución anterior no le permitirá obtener la propiedad de un const
objeto:
const Foo1 f;
X x = f.get_x(); // Compiler error, but it should be possible
Esto es porque get_x
no ser un método const no se puede llamar en un objeto const. Lo racional para esto es que un método no constante puede modificar el objeto, por lo que es ilegal llamarlo en un objeto constante.
Por eso hacemos los ajustes necesarios:
class Foo2
private:
X x_;
public:
void set_x(const X& value)
x_ = value;
const X& get_x() const
// ^~~~~
return x_;
;
La variante anterior es correcta. Sin embargo, en C ++ hay otra forma de escribirlo que es más C ++ y menos Java.
Hay dos cosas a considerar:
- podemos devolver una referencia al miembro de datos y si modificamos esa referencia, en realidad modificamos el miembro de datos en sí. Podemos usar esto para escribir nuestro setter.
- en C ++, los métodos se pueden sobrecargar solo con la constancia.
Entonces, con el conocimiento anterior, podemos escribir nuestra elegante versión final de C ++:
Versión definitiva
class Foo
private:
X x_;
public:
X& x() return x_;
const X& x() const return x_;
;
Como preferencia personal, utilizo el nuevo estilo de función de retorno final. (por ejemplo, en lugar de int foo()
yo escribo auto foo() -> int
.
class Foo
private:
X x_;
public:
auto x() -> X& return x_;
auto x() const -> const X& return x_;
;
Y ahora cambiamos la sintaxis de llamada de:
Foo2 f;
X x1;
f.set_x(x1);
X x2 = f.get_x();
para:
Foo f;
X x1;
f.x() = x1;
X x2 = f.x();
const Foo cf;
X x1;
//cf.x() = x1; // error as expected. We cannot modify a const object
X x2 = cf.x();
Más allá de la versión final
Por motivos de rendimiento, podemos dar un paso más y sobrecargarnos &&
y devolver una referencia rvalue a x_
, permitiendo así moverse de él si es necesario.
class Foo
private:
X x_;
public:
auto x() const& -> const X& return x_;
auto x() & -> X& return x_;
auto x() && -> X&& return std::move(x_);
;
Muchas gracias por los comentarios recibidos en los comentarios y en particular a StorryTeller por sus excelentes sugerencias para mejorar esta publicación.
Si conservas algún dilema y disposición de ascender nuestro crónica eres capaz de realizar una crónica y con mucho gusto lo leeremos.