principiantes - lista de codigos c++
Evite la duplicación de código al usar C++ 11 copiar y mover (3)
La solución de usar una referencia de reenvío es buena. En algunos casos se vuelve difícil o molesto. Como primer paso, envuélvalo con una interfaz que tome tipos explícitos y luego, en el archivo cpp, envíelos a una implementación de plantilla.
Ahora, a veces, el primer paso también falla: si hay N argumentos diferentes que todos deben enviarse a un contenedor, esto requiere una interfaz de tamaño 2 ^ N, y posiblemente tenga que cruzar varias capas de interfaces para llegar a la implementación .
Para ese fin, en lugar de llevar o tomar tipos específicos, podemos llevar la acción final alrededor. En la interfaz más externa, convertimos tipos arbitrarios en esa / esas acción (es).
template<class T>
struct construct {
T*(*action)(void* state,void* target)=nullptr;
void* state=nullptr;
construct()=default;
construct(T&& t):
action(
[](void*src,void*targ)->T*{
return new(targ) T( std::move(*static_cast<T*>(src)) );
}
),
state(std::addressof(t))
{}
construct(T const& t):
action(
[](void*src,void*targ)->T*{
return new(targ) T( *static_cast<T const*>(src) );
}
),
state(const_cast<void*>(std::addressof(t)))
{}
T*operator()(void* target)&&{
T* r = action(state,target);
*this = {};
return r;
}
explicit operator bool()const{return action;}
construct(construct&&o):
construct(o)
{
action=nullptr;
}
construct& operator=(construct&&o){
*this = o;
o.action = nullptr;
return *this;
}
private:
construct(construct const&)=default;
construct& operator=(construct const&)=default;
};
Una vez que tenga un objeto construct<T> ctor
, puede construir una instancia de T
través de std::move(ctor)(location)
, donde la ubicación es un puntero correctamente alineado para almacenar una T
con suficiente almacenamiento.
Un constructor<T>
se puede convertir implícitamente a partir de un valor r o valor T
. También se puede mejorar con el soporte de emplazamientos, pero eso requiere un montón más de repetición para hacerlo correctamente (o más gastos generales para hacerlo fácilmente).
Ejemplo vivo . El patrón es un borrado de tipo relativamente simple. Almacenamos la operación en un puntero de función, y los datos en un puntero de vacío, y reconstruimos los datos del puntero de vacío en el puntero de función de acción almacenada.
Hay un costo modesto en la técnica anterior de conceptos de borrado / tiempo de ejecución.
También podemos implementarlo así:
template<class T>
struct construct :
private std::function< T*(void*) >
{
using base = std::function< T*(void*) >;
construct() = default;
construct(T&& t):base(
[&](void* target)mutable ->T* {
return new(target) T(std::move(t));
}
) {}
construct(T const& t):base(
[&](void* target)->T* {
return new(target) T(t);
}
) {}
T* operator()(void* target)&&{
T* r = base::operator()(target);
(base&)(*this)={};
return r;
}
explicit operator bool()const{
return (bool)static_cast<base const&>(*this);
}
};
que se basa en std::function
haciendo el borrado de tipo para nosotros.
Como esto está diseñado para funcionar solo una vez (nos movemos de la fuente), fuerzo un contexto de valor y elimino mi estado. También oculto el hecho de que soy una función estándar, porque no sigue esas reglas.
C ++ 11 "mover" es una buena característica, pero me resultó difícil evitar la duplicación de código (todos odiamos esto) cuando se usa con "copiar" al mismo tiempo. El siguiente código es mi implementación de una cola circular simple (incompleta), los dos métodos push () son casi iguales, excepto una línea.
Me he encontrado con muchas situaciones similares como esta. ¿Alguna idea de cómo evitar este tipo de duplicación de código sin usar macro?
=== EDIT ===
En este ejemplo particular, el código duplicado se puede refactorizar y colocar en una función separada, pero a veces este tipo de refactorización no está disponible o no se puede implementar fácilmente.
#include <cstdlib>
#include <utility>
template<typename T>
class CircularQueue {
public:
CircularQueue(long size = 32) : size{size} {
buffer = std::malloc(sizeof(T) * size);
}
~CircularQueue();
bool full() const {
return counter.in - counter.out >= size;
}
bool empty() const {
return counter.in == counter.out;
}
void push(T&& data) {
if (full()) {
throw Invalid{};
}
long offset = counter.in % size;
new (buffer + offset) T{std::forward<T>(data)};
++counter.in;
}
void push(const T& data) {
if (full()) {
throw Invalid{};
}
long offset = counter.in % size;
new (buffer + offset) T{data};
++counter.in;
}
private:
T* buffer;
long size;
struct {
long in, out;
} counter;
};
La solución más simple aquí es hacer que el parámetro sea una referencia de reenvío. De esta manera usted puede salirse con una sola función:
template <class U>
void push(U&& data) {
if (full()) {
throw Invalid{};
}
long offset = counter.in % size;
// please note here we construct a T object (the class template)
// from an U object (the function template)
new (buffer + offset) T{std::forward<U>(data)};
++counter.in;
}
Hay desventajas con el método sin embargo:
No es genérico, es decir, no siempre se puede hacer (de manera trivial). Por ejemplo, cuando el parámetro no es tan simple como T (por ejemplo,
SomeType<T>
).Retrasas la comprobación de tipo del parámetro. Un error largo y aparentemente no relacionado con el compilador puede seguir cuando se llama a push con un tipo de parámetro incorrecto.
Por cierto, en su ejemplo, T&&
no es una referencia de reenvío. Es una referencia de valor. Eso es porque T no es un parámetro de plantilla de la función. Es de la clase, por lo que ya se deduce cuando se crea una instancia de la clase. Así que la forma correcta de escribir su código habría sido:
void push(T&& data) {
...
... T{std::move(data)};
...
}
void push(const T& data) {
... T{data};
...
}
Prefacio
La introducción de duplicación de código al agregar soporte de semántica de movimiento a su interfaz es muy molesta. Para cada función debe realizar dos implementaciones casi idénticas: la que se copia del argumento y la que se mueve del argumento. Si una función tiene dos parámetros, ni siquiera es duplicación de código, se trata de una cuadruplicación de código:
void Func(const TArg1 &arg1, const TArg2 &arg2); // copies from both arguments
void Func(const TArg1 &arg1, TArg2 &&arg2); // copies from the first, moves from the second
void Func( TArg1 &&arg1, const TArg2 &arg2); // moves from the first, copies from the second
void Func( TArg1 &&arg1, TArg2 &&arg2); // moves from both
En el caso general, debe realizar hasta 2 ^ N sobrecargas para una función donde N es el número de parámetros. En mi opinión, esto hace que la semántica del movimiento sea prácticamente inutilizable. Es la característica más decepcionante de C ++ 11.
El problema podría haber ocurrido incluso antes. Echemos un vistazo a la siguiente pieza de código:
void Func1(const T &arg);
T Func2();
int main()
{
Func1(Func2());
return 0;
}
Es bastante extraño que un objeto temporal pase a la función que toma una referencia. El objeto temporal puede incluso no tener una dirección, por ejemplo, puede almacenarse en caché en un registro. Pero C ++ permite pasar temporarios donde se acepta una referencia const (y solo const). En ese caso, la vida útil temporal se prolonga hasta el final de la vida útil de la referencia. Si no hubiera esta regla, tendríamos que hacer dos implementaciones incluso aquí:
void Func1(const T& arg);
void Func1(T arg);
No sé por qué se creó la regla que permite pasar temporarios donde se acepta la referencia (bueno, si no existiera esta regla, no podríamos llamar al constructor de copia para hacer una copia de un objeto temporal, por Func1(Func2())
tanto Func1(Func2())
donde Func1
es void Func1(T arg)
no funcionaría de todos modos :)), pero con esta regla no tenemos que realizar dos sobrecargas de la función.
Solución # 1: reenvío perfecto
Desafortunadamente, no existe una regla tan simple que haría innecesario implementar dos sobrecargas de la misma función: la que toma una referencia de valor constante y la que toma la referencia de valor r. En su lugar se ideó un reenvío perfecto
template <typename U>
void Func(U &¶m) // despite the fact the parameter has "U&&" type at declaration,
// it actually can be just "U&" or even "const U&", it’s due to
// the template type deducing rules
{
value = std::forward<U>(param); // use move or copy semantic depending on the
// real type of param
}
Puede parecer esa simple regla que permite evitar la duplicación. Pero no es simple, usa una plantilla "mágica" no obvia para resolver el problema, y también esta solución tiene algunas desventajas que se derivan del hecho de que la función que utiliza el reenvío perfecto debe tener una plantilla:
- La implementación de la función debe estar ubicada en un encabezado.
- Infla el tamaño binario porque para cada combinación utilizada del tipo de parámetros (copiar / mover) genera una implementación separada (tiene una implementación única en el código fuente y al mismo tiempo tiene implementaciones de hasta 2 ^ N en el binario) .
- No hay comprobación de tipo para el argumento. Puede pasar valor de cualquier tipo a la función (ya que la función acepta el tipo de plantilla). La comprobación real se realizará en los puntos en los que realmente se utiliza el parámetro. Esto puede producir mensajes de error difíciles de entender y llevar a algunas consecuencias inesperadas.
El último problema se puede resolver creando envoltorios sin plantillas para funciones de reenvío perfecto:
public:
void push( T &&data) { push_fwd(data); }
void push(const T &data) { push_fwd(data); }
private:
template <typename U>
void push_fwd(U &&data)
{
// actual implementation
}
Por supuesto, solo se puede utilizar en la práctica si la función tiene pocos parámetros (uno o dos). De lo contrario, tiene que hacer demasiados envoltorios (hasta 2 ^ N, ya sabe).
Solución # 2: Verificación en tiempo de ejecución para la movilidad
Finalmente, llegué a la idea de que la verificación de los argumentos para la movilidad no debería hacerse en tiempo de compilación sino en tiempo de ejecución. Creé una clase de envoltura de referencia con constructores que tomaron ambos tipos de referencias (rvalue y const lvalue). La clase almacenó la referencia pasada al constructor como una referencia de valor constante y adicionalmente almacenó la bandera si la referencia pasada era rvalue. Luego, puede verificar en tiempo de ejecución si la referencia original era rvalue y, si es así, acaba de convertir la referencia almacenada en rvalue-reference.
Como era de esperar, alguien más había tenido esta idea antes que yo. Llamó a esto como "en el idioma" (yo lo llamé "pmp" - posiblemente param móvil). Puede leer sobre este idioma en detalles here y here (página original sobre "en" idioma, recomiendo leer las 3 partes del artículo si está realmente interesado en el problema, el artículo lo analiza en profundidad).
En resumen, la implementación del lenguaje se ve así:
template <typename T>
class in
{
public:
in (const T& l): v_ (l), rv_ (false) {}
in (T&& r): v_ (r), rv_ (true) {}
bool rvalue () const {return rv_;}
const T& get () const {return v_;}
T&& rget () const {return std::move (const_cast<T&> (v_));}
private:
const T& v_; // original reference
bool rv_; // whether it is rvalue-reference
};
(La implementación completa también contiene un caso especial cuando algunos tipos pueden convertirse implícitamente en T)
Ejemplo de uso:
class A
{
public:
void set_vec(in<std::vector<int>> param1, in<std::vector<int>> param2)
{
if (param1.rvalue()) vec1 = param1.rget(); // move if param1 is rvalue
else vec1 = param1.get(); // just copy otherwise
if (param2.rvalue()) vec2 = param2.rget(); // move if param2 is rvalue
else vec2 = param2.get(); // just copy otherwise
}
private:
std::vector<int> vec1, vec2;
};
La implementación de "in" carece de constructores de copiar y mover.
class in
{
...
in(const in &other): v_(other.v_), rv_(false) {} // always makes parameter not movable
// even if the original reference
// is movable
in( in &&other): v_(other.v_), rv_(other.rv_) {} // makes parameter movable if the
// original reference was is movable
...
};
Ahora podemos usarlo de esta manera:
void func1(in<std::vector<int>> param);
void func2(in<std::vector<int>> param);
void func3(in<std::vector<int>> param)
{
func1(param); // don''t move param into func1 even if original reference
// is rvalue. func1 will always use copy of param, since we
// still need param in this function
// some usage of param
// now we don’t need param
func2(std::move(param)); // move param into func2 if original reference
// is rvalue, or copy param into func2 if original
// reference is const lvalue
}
También podríamos sobrecargar un operador de asignación:
template<typename T>
T& operator=(T &lhs, in<T> rhs)
{
if (rhs.rvalue()) lhs = rhs.rget();
else lhs = rhs.get();
return lhs;
}
Después de eso, no tendríamos que buscar ravlue cada vez, solo podríamos usarlo de esta manera:
vec1 = std::move(param1); // moves or copies depending on whether param1 is movable
vec2 = std::move(param2); // moves or copies depending on whether param2 is movable
Pero, desafortunadamente, C ++ no permite la sobrecarga del operator=
como función global ( https://.com/a/871290/5447906 ). Pero podemos cambiar el nombre de esta función para assign
:
template<typename T>
void assign(T &lhs, in<T> rhs)
{
if (rhs.rvalue()) lhs = rhs.rget();
else lhs = rhs.get();
}
y úsalo así:
assign(vec1, std::move(param1)); // moves or copies depending on whether param1 is movable
assign(vec2, std::move(param2)); // moves or copies depending on whether param2 is movable
Además esto no funcionará con constructores. No podemos simplemente escribir:
std::vector<int> vec(std::move(param));
Esto requiere la biblioteca estándar para admitir esta característica:
class vector
{
...
public:
vector(std::in<vector> other); // copy and move constructor
...
}
Pero los estándares no saben nada acerca de nuestra clase "en". Y aquí no podemos hacer una solución similar para assign
, por lo que el uso de la clase "in" es limitado.
Epílogo
T
, const T&
, T&&
para los parámetros es demasiado para mí. Deja de introducir cosas que hagan lo mismo (bueno, casi lo mismo). T
es suficiente!
Preferiría escribir así:
// The function in ++++C language:
func(std::vector<int> param) // no need to specify const & or &&, param is just parameter.
// it is always reference for complex types (or for types with
// special qualifier that says that arguments of this type
// must be always passed by reference).
{
another_vec = std::move(param); // move parameter if it''s movable.
// compiler hides actual rvalue-ness
// of the arguments in its ABI
}
No sé si el comité estándar consideró este tipo de implementación de semántica de movimientos, pero probablemente sea demasiado tarde para realizar tales cambios en C ++ porque harían que el ABI de los compiladores fuera incompatible con versiones anteriores. Además, agrega cierta sobrecarga de tiempo de ejecución, y puede haber otros problemas que no conocemos.