2011-06-16 14 views
5

Quiero sobrecargar una función para que manipule su argumento de alguna manera y luego devuelve una referencia al argumento, pero si el argumento no es mutable, entonces debe devolver una copia manipulada del argumento en lugar. Después de perder el tiempo por años, esto es lo que he pensado.Sobrecarga de la función rvalue

using namespace std; 

string& foo(string &in) 
{ 
    in.insert(0, "hello "); 
    return in; 
} 

string foo(string &&in) 
{ 
    return move(foo(in)); 
} 

string foo(const string& in) 
{ 
    return foo(string(in)); 
} 

Este código parece funcionar correctamente, pero estoy interesado en saber si alguien puede pensar en una mejor manera de hacerlo.

Aquí es un programa de prueba:

int main(void) 
{ 
    string var = "world"; 
    const string var2 = "const world"; 
    cout << foo(var) << endl; 
    cout << var << endl; 

    cout << foo(var2) << endl; 
    cout << var2 << endl; 

    cout << foo(var + " and " + var2) << endl; 
    return 0; 
} 

La salida correcta es

hello world 
hello world 
hello const world 
const world 
hello hello world and const world 

Calculo que sería un poco más ordenado si podía hacer esto:

string& foo(string &in) 
{ 
    in.insert(0, "hello "); 
    return in; 
} 

string foo(string in) 
{ 
    return move(foo(in)); 
} 

Por supuesto, eso no funciona porque la mayoría de las llamadas a funciones en foo serían ambiguas, incluida la llamada en foo sí mismo! Pero si de alguna manera pudiera decirle al compilador que priorice el primero ...

Como dije, el código que publiqué funciona correctamente. Lo principal que no me gusta de él es el código adicional repetitivo. Si tuviera un montón de funciones así, se convertiría en un desastre, y la mayoría serían muy repetitivas. Entonces, como segunda parte de mi pregunta: ¿alguien puede pensar en una forma de generar automáticamente el código para la segunda y tercera función foo? por ejemplo,

// implementation of magic_function_overload_generator 
// ??? 

string& foo(string &in); 
magic_function_overload_generator<foo>; 

string& bar(string &in); 
magic_function_overload_generator<bar>; 

// etc 
+7

Esto suena aterrador. Dependiendo de qué tipo pase a la función, el estado resultante del valor de retorno * y el parámetro * podría ser completamente diferente. Eso es solo pedir errores sutiles. ¿Por qué no dejar que el usuario decida si quiere modificar el objeto en su lugar o devolver una copia llamando explícitamente a diferentes funciones? – jalf

+0

No me parece particularmente aterrador, pero quizás tengas razón. La forma en que pienso es que la función cambia la entrada siempre que sea capaz; pero si no puede ... no es así, pero todavía da el valor de retorno correcto. El tipo de cosas que podría usar es algo así como una función "Puntuar", que toma una cadena mal puntuada y la corrige. Es posible que desee enviar el resultado directamente a cout, o quizás desee realizar algunas otras operaciones en la cadena después. Entonces, a veces puedes pasar valores constantes, y algunas veces ... bueno, entiendes la idea. – karadoc

+1

pero mi punto es que si uno o el otro pasa no depende de lo que el programador quiere, sino de algunos detalles semánticos relativamente sutiles (¿el tipo de argumento es const o no? ¿Es un valor r o no?), que podría cambiar fácilmente con el tiempo sin que el programador tome la decisión explícita de que "ahora quiero devolver una copia, en lugar de modificar el objeto en su lugar". Entiendo lo que intenta hacer, pero es una decisión que el programador puede tomar fácilmente, y que hacer que su biblioteca adivine erróneamente puede tener consecuencias potencialmente muy malas. – jalf

Respuesta

4

me desharía de las referencias todos juntos y acaba de escribir una función que pasa y vuelve por valor:

std::string foo(std::string in) 
{ 
    in.insert(0, "hello "); 
    return in; 
} 

Si pasa un valor-I, se copiará la cadena de entrada. Si pasa un valor r, se moverá.

Al salir de la función, la optimización del valor de retorno probablemente se activará, por lo que la devolución es básicamente no operativa. Si el compilador decide no hacerlo, el resultado se moverá (aunque in es un valor l).

Lo bueno de las referencias de rvalue es que tienes que pensar menos sobre dónde poner referencias en el código de usuario para ganar eficiencia. Con los tipos móviles, pasar-por-valor es prácticamente tan eficiente como se pone.

+0

Esto es básicamente lo que terminé haciendo. Jalf y otros me han convencido de que estaba sobrediseñando todo. Una función no necesita hacer tanto las versiones mutables como inmutables, y es un hecho probablemente más confuso si una función hace ambas cosas. Supongo que me sobreexcitó la idea de usar referencias rvalue para hacer algo genial. ¡Ojalá nunca hubiera oído hablar de referencias de valores, entonces habría elegido esta solución y no habría perdido medio día tratando de hacer algo elegante! – karadoc

+0

No se preocupe, las cosas buenas todavía suceden, debajo del capó;) – fredoverflow

1

¿Qué hay del siguiente enfoque simple?

string& foo (string &change) // this accepts mutable string 
{ 
    change = string("hello ") + change; 
    return change; 
} 

string foo (const string &unchange) // this accepts not mutable string 
{ 
    return string("hello ") + unchange; 
} 

See it's output here.

+0

Lo malo de este enfoque es que el cuerpo de la función debe escribirse dos veces. En este ejemplo en particular es mejor que lo que tengo, pero si foo es algo largo y complejo, entonces duplicaría el código requerido. – karadoc

+2

@karadoc: puede escribir fácilmente el no mutante en términos del mutante. 'int notModifying (const int & i) {int ii = i; volver a modificar (ii); } ' –

0

En la misma línea que la respuesta de @ iammilind, pero sans duplicación:

#include <iostream> 
using namespace std; 

string foo(const string &unchange) { 
    return string("hello ") + unchange; 
} 

string& foo(string &change) { 
    return change = foo(static_cast<const string&>(foo)); 
} 

int main(int argc, char** argv) { 
    string a = "world"; 
    const string b = "immutable world"; 
    cout << foo(a) << '\n' << foo(b) << '\n'; 
    cout << foo(a) << '\n' << foo(b) << '\n'; 
} 

Nota: También puede utilizar const_cast aquí para añadir la calificación const.

2

Toda la pregunta es ¿por qué quieres tener tales sobrecargas? Todas estas sobrecargas especifican una interfaz: foo (x). Pero x parameter puede ser input o input/output parámetro según su tipo. Es muy, muy propenso a errores. Un usuario debe hacer un trabajo adicional para asegurarse de que su variable no se mutará. Nunca hagas eso en el código de producción.

estoy de acuerdo con este tipo de sobrecargas:

string foo(string &&in); 
string foo(const string& in); 

parámetro de entrada no se cambia nunca si no es un temporal y, al mismo tiempo, se vuelve a utilizar objetos temporales. Parece bastante razonable.

Pero, ¿por qué quiere generar muchas de esas sobrecargas? & & sobrecarga es para la optimización. Diría una optimización muy delicada. ¿Estás seguro de que lo necesitas en muchos lugares?

De todos modos, si realmente desea generar código C++, las plantillas no son una buena opción. Usaría alguna herramienta externa para eso. Personalmente, prefiero Cog.

0

Si no está preocupado por la eficiencia, puede hacer pasar por valor o pasar por referencia constante y hacer una copia y terminar con ello.

Sin embargo, si le preocupa la eficiencia, no creo que la sugerencia de valor de paso en este reply sea el mejor enfoque. Esto se debe a que creo que da como resultado copias/movimientos adicionales, ya que NRVO solo parece funcionar con variables locales, no con parámetros. Creo que la forma que evite movimientos/copias en C++ 0x es el doble sobrecargas, como lo ilustra el siguiente código:

#include <iostream> 

struct A 
{ 
    A() : i(0) {} 
    A(const A& x) : i(x.i) { std::cout << "Copy" << std::endl; } 
    A(A&& x) : i(x.i) { std::cout << "Move" << std::endl; } 
    void inc() { ++i; } 
    int i; 
}; 

A f1(const A& x2) { A x = x2; x.inc(); return x; } 
A&& f1(A&& x) { x.inc(); return std::move(x); } 

A f2(A x) { x.inc(); return std::move(x); } 

int main() 
{ 
    A x; 
    std::cout << "A a1 = f1(x);" << std::endl; 
    A a1 = f1(x); 
    std::cout << "A a2 = f1(A());" << std::endl; 
    A a2 = f1(A()); 
    std::cout << "A b1 = f2(x);" << std::endl; 
    A b1 = f2(x); 
    std::cout << "A b2 = f2(A());" << std::endl; 
    A b2 = f2(A()); 
    std::cout << std::endl; 
    std::cout << "A a3 = f1(f1(x));" << std::endl; 
    A a3 = f1(f1(x)); 
    std::cout << "A a4 = f1(f1(A()));" << std::endl; 
    A a4 = f1(f1(A())); 
    std::cout << "A b3 = f2(f2(x));" << std::endl; 
    A b3 = f2(f2(x)); 
    std::cout << "A b4 = f2(f2(A()));" << std::endl; 
    A b4 = f2(f2(A())); 
    std::cout << std::endl; 
    std::cout << "A a5 = f1(f1(f1(x)));" << std::endl; 
    A a5 = f1(f1(f1(x))); 
    std::cout << "A a6 = f1(f1(f1(A())));" << std::endl; 
    A a6 = f1(f1(f1(A()))); 
    std::cout << "A b5 = f2(f2(f2(x)));" << std::endl; 
    A b5 = f2(f2(f2(x))); 
    std::cout << "A b6 = f2(f2(f2(A())));" << std::endl; 
    A b6 = f2(f2(f2(A()))); 
} 

que produce los siguientes resultados:

A a1 = f1(x); 
Copy 
A a2 = f1(A()); 
Move 
A b1 = f2(x); 
Copy 
Move 
A b2 = f2(A()); 
Move 

A a3 = f1(f1(x)); 
Copy 
Move 
A a4 = f1(f1(A())); 
Move 
A b3 = f2(f2(x)); 
Copy 
Move 
Move 
A b4 = f2(f2(A())); 
Move 
Move 

A a5 = f1(f1(f1(x))); 
Copy 
Move 
A a6 = f1(f1(f1(A()))); 
Move 
A b5 = f2(f2(f2(x))); 
Copy 
Move 
Move 
Move 
A b6 = f2(f2(f2(A()))); 
Move 
Move 
Move 

es posible que pueda hacer algunos trucos plantilla para evitar escribir múltiples sobrecargas, por ejemplo:

template <class T> 
param_return_type<T&&>::type f3(T&& y, typename std::enable_if<...>::type* dummy = 0) 
{ 
    typedef return_t param_return_type<T&&>::type; 
    return_t x = static_cast<return_t>(y); 
    x.inc(); 
    return static_cast<return_t>(x); 
} 

Dónde param_return_type<T>::type es T cuando se pasa (const) T&, y T&& cuando pasó T&&. std::enable_if<...> puede usar si solo desea que esta plantilla tome parámetros particulares. No estaba seguro de cómo escribir una definición de param_return_type<T>::type, ya que parece que no hay std::remove_lvalue_reference. Si alguien sabe cómo hacerlo, siéntase libre de editar/agregar a mi publicación.