c++ - una - El uso de referencias de valor en el parámetro de función de la función sobrecargada crea demasiadas combinaciones
tipos de funciones en c (7)
Imagina que tienes una serie de métodos sobrecargados que (antes de C ++ 11) se veían así:
class MyClass {
public:
void f(const MyBigType& a, int id);
void f(const MyBigType& a, string name);
void f(const MyBigType& a, int b, int c, int d);
// ...
};
Esta función hace una copia de a
( MyBigType
), por lo que quiero agregar una optimización al proporcionar una versión de f
que mueve a
lugar de copiarla.
Mi problema es que ahora el número de sobrecargas de f
se duplicará:
class MyClass {
public:
void f(const MyBigType& a, int id);
void f(const MyBigType& a, string name);
void f(const MyBigType& a, int b, int c, int d);
// ...
void f(MyBigType&& a, int id);
void f(MyBigType&& a, string name);
void f(MyBigType&& a, int b, int c, int d);
// ...
};
Si tuviera más parámetros que pudieran moverse, no sería práctico proporcionar todas las sobrecargas.
¿Alguien ha tratado este tema? ¿Hay una buena solución / patrón para resolver este problema?
¡Gracias!
¿Por qué harías eso?
Estas sobrecargas adicionales solo tienen sentido si la modificación de los parámetros de función en la implementación de la función realmente le otorga una ganancia de rendimiento significativa (o algún tipo de garantía). Este casi nunca es el caso, excepto en el caso de constructores u operadores de asignación. Por lo tanto, le aconsejo que reconsidere, si realmente es necesario poner estas sobrecargas.
Si las implementaciones son casi idénticas ...
Desde mi experiencia, esta modificación es simplemente pasar el parámetro a otra función envuelta en std::move()
y el resto de la función es idéntica a la const &
versión. En ese caso, puede convertir su función en una plantilla de este tipo:
template <typename T> void f(T && a, int id);
Luego, en la implementación de la función, simplemente reemplaza la operación std::move(a)
con std::forward<T>(a)
y debería funcionar. Puede restringir el tipo de parámetro T
con std::enable_if
, si lo desea.
En el caso de referencia constante: No cree un temporal, solo para modificarlo
Si en el caso de referencias constantes crea una copia de su parámetro y luego continúa de la misma manera que funciona la versión de movimiento, entonces puede pasar el parámetro por valor y usar la misma implementación que usó para la versión de movimiento.
void f( MyBigData a, int id );
Por lo general, esto le dará el mismo rendimiento en ambos casos y solo necesita una sobrecarga e implementación. ¡Muchas ventajas!
Implementaciones significativamente diferentes.
En caso de que las dos implementaciones difieran significativamente, no hay una solución genérica que yo sepa. Y creo que no puede haber ninguno. Este es también el único caso en el que hacer esto realmente tiene sentido, si el rendimiento del perfil muestra mejoras adecuadas.
Esta es la parte crítica de la pregunta:
Esta función hace una copia de un (MyBigType),
Lamentablemente, es un poco ambiguo. Nos gustaría saber cuál es el objetivo final de los datos en el parámetro. Lo es:
- 1) para ser asignado a un objeto que existía antes de que se llamara
f
? - 2) o en su lugar, almacenado en una variable local:
es decir:
void f(??? a, int id) {
this->x = ??? a ???;
...
}
o
void f(??? a, int id) {
MyBigType a_copy = ??? a ???;
...
}
A veces, la primera versión (la tarea) se puede hacer sin ninguna copia o movimiento. Si esta - this->x
ya es una string
larga, y si a
es corta, entonces puede reutilizar eficientemente la capacidad existente. Sin copia-construcción, y sin movimientos. En resumen, a veces la asignación puede ser más rápida porque podemos omitir la construcción de copias.
De todos modos, aquí va:
template<typename T>
void f(T&& a, int id) {
this->x = std::forward<T>(a); // is assigning
MyBigType local = std::forward<T>(a); // if move/copy constructing
}
Herb Sutter habla sobre algo similar en una charla de cppcon
Esto se puede hacer, pero probablemente no debería. Puede obtener el efecto utilizando referencias y plantillas universales, pero desea restringir el tipo a MyBigType
y las cosas que se pueden convertir implícitamente a MyBigType
. Con algunos trucos tmp, puedes hacer esto:
class MyClass {
public:
template <typename T>
typename std::enable_if<std::is_convertible<T, MyBigType>::value, void>::type
f(T&& a, int id);
};
El único parámetro de la plantilla coincidirá con el tipo real del parámetro, el tipo de retorno enable_if
no permite tipos incompatibles. Lo desarmaré pieza por pieza
std::is_convertible<T, MyBigType>::value
Esta expresión de tiempo de compilación se evaluará como true
si T
se puede convertir implícitamente a un MyBigType
. Por ejemplo, si MyBigType
fuera un std::string
y T fuera un char*
la expresión sería verdadera, pero si T fuera un int
sería falsa.
typename std::enable_if<..., void>::type // where the ... is the above
esta expresión se void
en caso de que la expresión is_convertible
sea verdadera. Cuando es falso, la expresión tendrá un formato incorrecto, por lo que la plantilla se desechará.
Dentro del cuerpo de la función, deberá utilizar el reenvío perfecto. Si planea asignar copias o mover objetos, el cuerpo sería algo así como
{
this->a_ = std::forward<T>(a);
}
Aquí hay un ejemplo de coliru live con un using MyBigType = std::string
. Como dice Herb, esta función no puede ser virtual y debe implementarse en el encabezado. Los mensajes de error que recibe al llamar con un tipo incorrecto serán bastante aproximados en comparación con las sobrecargas sin plantilla.
Gracias al comentario de Barry por esta sugerencia, para reducir la repetición, probablemente sea una buena idea crear un alias de plantilla para el mecanismo SFINAE. Si declaras en tu clase.
template <typename T>
using EnableIfIsMyBigType = typename std::enable_if<std::is_convertible<T, MyBigType>::value, void>::type;
entonces usted podría reducir las declaraciones a
template <typename T>
EnableIfIsMyBigType<T>
f(T&& a, int id);
Sin embargo, esto supone que todas sus sobrecargas tienen un tipo de retorno void
. Si el tipo de retorno difiere, podría usar un alias de dos argumentos en su lugar
template <typename T, typename R>
using EnableIfIsMyBigType = typename std::enable_if<std::is_convertible<T, MyBigType>::value,R>::type;
Luego declara con el tipo de retorno especificado
template <typename T>
EnableIfIsMyBigType<T, void> // void is the return type
f(T&& a, int id);
La opción ligeramente más lenta es tomar el argumento por valor. Si lo haces
class MyClass {
public:
void f(MyBigType a, int id) {
this->a_ = std::move(a); // move assignment
}
};
En el caso de que f
pase un valor l, copiará la construcción a
de su argumento y luego la asignará a this->a_
. En el caso de que f
pase un rvalue, moverá la construcción a
del argumento y luego moverá la asignación. Un ejemplo vivo de este comportamiento está here . Tenga en cuenta que yo uso -fno-elide-constructors
, sin esa bandera, los casos de valor elide la construcción del movimiento y solo la asignación de movimiento tiene lugar.
Si es costoso mover el objeto ( std::array
por ejemplo), este enfoque será notablemente más lento que la primera versión super-optimizada. Además, considere ver esta parte de la charla de Herb a la que Chris Drew se vincula en los comentarios para comprender cuándo podría ser más lento que usar referencias. Si tiene una copia de Effective Modern C ++ de Scott Meyers , analiza los altibajos en el artículo 41.
Mi primer pensamiento es que debes cambiar los parámetros para pasar por valor. Esto cubre la necesidad existente de copiar, excepto que la copia ocurre en el punto de llamada en lugar de explícitamente en la función. También permite que los parámetros se creen por construcción de movimiento en un contexto movible (ya sea temporarios sin nombre o usando std::move
).
Podría introducir un objeto mutable:
#include <memory>
#include <type_traits>
// Mutable
// =======
template <typename T>
class Mutable
{
public:
Mutable(const T& value) : m_ptr(new(m_storage) T(value)) {}
Mutable(T& value) : m_ptr(&value) {}
Mutable(T&& value) : m_ptr(new(m_storage) T(std::move(value))) {}
~Mutable() {
auto storage = reinterpret_cast<T*>(m_storage);
if(m_ptr == storage)
m_ptr->~T();
}
Mutable(const Mutable&) = delete;
Mutable& operator = (const Mutable&) = delete;
const T* operator -> () const { return m_ptr; }
T* operator -> () { return m_ptr; }
const T& operator * () const { return *m_ptr; }
T& operator * () { return *m_ptr; }
private:
T* m_ptr;
char m_storage[sizeof(T)];
};
// Usage
// =====
#include <iostream>
struct X
{
int value = 0;
X() { std::cout << "default/n"; }
X(const X&) { std::cout << "copy/n"; }
X(X&&) { std::cout << "move/n"; }
X& operator = (const X&) { std::cout << "assign copy/n"; return *this; }
X& operator = (X&&) { std::cout << "assign move/n"; return *this; }
~X() { std::cout << "destruct " << value << "/n"; }
};
X make_x() { return X(); }
void fn(Mutable<X>&& x) {
x->value = 1;
}
int main()
{
const X x0;
std::cout << "0:/n";
fn(x0);
std::cout << "1:/n";
X x1;
fn(x1);
std::cout << "2:/n";
fn(make_x());
std::cout << "End/n";
}
Puedes hacer algo como lo siguiente.
class MyClass {
public:
void f(MyBigType a, int id) { this->a = std::move(a); /*...*/ }
void f(MyBigType a, string name);
void f(MyBigType a, int b, int c, int d);
// ...
};
Solo tienes un move
extra (que puede ser optimizado).
Si la versión de movimiento proporcionará una optimización, entonces la implementación de la función de sobrecarga de movimiento y la copia deben ser realmente diferentes. No veo una manera de evitar esto sin proporcionar implementaciones para ambos.