C++ 0x referencias de valores-lvalues-rvalue binding
c++11 rvalue-reference (2)
Esta es una pregunta complementaria para C ++ 0x referencias de valor y temporales
En la pregunta anterior, pregunté cómo debería funcionar este código:
void f(const std::string &); //less efficient
void f(std::string &&); //more efficient
void g(const char * arg)
{
f(arg);
}
Parece que la sobrecarga de movimientos probablemente debería llamarse debido a un temporal implícito, y esto ocurre en GCC pero no en MSVC (o en el front-end EDG utilizado en Intellisense de MSVC).
¿Qué pasa con este código?
void f(std::string &&); //NB: No const string & overload supplied
void g1(const char * arg)
{
f(arg);
}
void g2(const std::string & arg)
{
f(arg);
}
Parece que, según las respuestas a mi pregunta anterior, esa función g1
es legal (y es aceptada por GCC 4.3-4.5, pero no por MSVC). Sin embargo, GCC y MSVC rechazan g2
debido a la cláusula 13.3.3.1.4 / 3, que prohíbe que los valores de enlace se vinculen con los valores de referencia de valor de r. Entiendo la razón detrás de esto: se explica en N2831 "Solución de un problema de seguridad con referencias de valor". También creo que GCC probablemente está implementando esta cláusula como pretenden los autores de ese artículo, porque el parche original para GCC fue escrito por uno de los autores (Doug Gregor).
Sin embargo, esto no es bastante intuitivo. Para mí, (a) una const string &
conceptualmente está más cerca de una string &&
que una const char *
, y (b) el compilador podría crear una cadena temporal en g2
, como si estuviera escrito así:
void g2(const std::string & arg)
{
f(std::string(arg));
}
De hecho, a veces se considera que el constructor de copia es un operador de conversión implícito. Sintácticamente, esto es sugerido por la forma de un constructor de copia, y el estándar incluso menciona esto específicamente en la cláusula 13.3.3.1.2 / 4, donde el constructor de copia para conversiones de base derivada recibe un rango de conversión más alto que otros conversiones:
A la conversión de una expresión de tipo de clase en el mismo tipo de clase se le otorga el rango de concordancia exacta, y a la conversión de una expresión de tipo de clase en una clase base de ese tipo se le asigna un rango de conversión, a pesar del hecho de que una copia / movimiento se llama al constructor (es decir, una función de conversión definida por el usuario) para esos casos.
(Supongo que esto se usa cuando se pasa una clase derivada a una función como void h(Base)
, que toma una clase base por valor).
Motivación
Mi motivación para hacer esto es algo parecido a la pregunta formulada en Cómo reducir el código redundante al agregar nuevas sobrecargas del operador de referencia de r ++ 0x r ++ ("Cómo reducir el código redundante al agregar nuevas sobrecargas del operador de referencia de r ++ 0x de c ++").
Si tiene una función que acepta una cantidad de argumentos potencialmente movibles, y los movería si pudiera (por ejemplo, una función / constructor de fábrica: Object create_object(string, vector<string>, string)
o similar), y desea mueva o copie cada argumento según corresponda, rápidamente comenzará a escribir muchos códigos.
Si los tipos de argumentos son movibles, entonces uno podría simplemente escribir una versión que acepte los argumentos por valor, como se indicó anteriormente. Pero si los argumentos son clases no legibles pero intercambiables (a la anterior) a la C ++ 03, y no puede cambiarlos, entonces escribir sobrecargas de referencia de valor es más eficiente.
Entonces, si lvalues se unía a rvalues a través de una copia implícita, podría escribir solo una sobrecarga como create_object(legacy_string &&, legacy_vector<legacy_string> &&, legacy_string &&)
y funcionaría más o menos como proporcionar todas las combinaciones de valor / valor sobrecargas de referencia: los argumentos reales que eran lvalues se copiarían y luego se unirían a los argumentos, los argumentos reales que eran rvalues se enlazarían directamente.
Aclaración / edición: me doy cuenta de que esto es prácticamente idéntico a aceptar argumentos por valor para tipos móviles, como C ++ 0x std :: string y std :: vector (excepto el número de veces que se invoca conceptualmente el constructor de movimientos). Sin embargo, no es idéntico para los tipos copiables, pero no móviles, que incluye todas las clases de C ++ 03 con constructores de copia definidos explícitamente. Considera este ejemplo:
class legacy_string { legacy_string(const legacy_string &); }; //defined in a header somewhere; not modifiable.
void f(legacy_string s1, legacy_string s2); //A *new* (C++0x) function that wants to move from its arguments where possible, and avoid copying
void g() //A C++0x function as well
{
legacy_string x(/*initialization*/);
legacy_string y(/*initialization*/);
f(std::move(x), std::move(y));
}
Si g
llama a f
, entonces se copiarían x
e y
, no veo cómo el compilador puede moverlos. Si f
se declarara en lugar de tomar argumentos legacy_string &&
, podría evitar esas copias en las que el llamante invocó explícitamente std::move
en los argumentos. No veo cómo estos son equivalentes.
Preguntas
Mis preguntas son entonces:
- ¿Es esta una interpretación válida de la norma? Parece que no es la convencional o la intencionada, por lo menos.
- ¿Tiene sentido intuitivo?
- ¿Hay algún problema con esta idea que no estoy viendo? Parece que se podrían crear copias en silencio cuando no se espera exactamente, pero ese es el status quo en lugares en C ++ 03 de todos modos. Las sobrecargas son viables cuando actualmente no lo están, pero no veo que sea un problema en la práctica.
- ¿Es esta una mejora tan significativa que valdría la pena realizar, por ejemplo, un parche experimental para GCC?
¿Qué pasa con este código?
void f(std::string &&); //NB: No const string & overload supplied
void g2(const std::string & arg)
{
f(arg);
}
... Sin embargo, tanto GCC como MSVC rechazan g2 debido a la cláusula 13.3.3.1.4 / 3, que prohíbe que los valores se vinculen a los argumentos de referencia de rvalue. Entiendo la razón detrás de esto: se explica en N2831 "Solución de un problema de seguridad con referencias de valor". También creo que GCC probablemente está implementando esta cláusula como pretenden los autores de ese artículo, porque el parche original para GCC fue escrito por uno de los autores (Doug Gregor) ...
No, eso es solo la mitad de la razón por la que ambos compiladores rechazan tu código. La otra razón es que no puede inicializar una referencia a non-const con una expresión que se refiere a un objeto const. Entonces, incluso antes de N2831 esto no funcionó. Simplemente no hay necesidad de una conversión porque una cadena ya es una cadena. Parece que quieres usar string&&
like string
. Luego, simplemente escriba su función f
para que tome una cadena por valor. Si desea que el compilador cree una copia temporal de un valor de cadena de const para que pueda invocar una función tomando una string&&
, no habría una diferencia entre tomar la cadena por valor o por rref, ¿verdad?
N2831 tiene poco que ver con este escenario.
Si tiene una función que acepta una cantidad de argumentos potencialmente movibles, y los movería si pudiera (por ejemplo, una función / constructor de fábrica: Objeto create_object (cadena, vector, cadena) o similar), y desea mover o copiar Cada argumento, según corresponda, rápidamente comienza a escribir muchos códigos.
Realmente no. ¿Por qué querrías escribir un montón de código? Hay pocas razones para saturar todo su código con sobrecargas const&
/ const&
. Aún puede usar una sola función con una combinación de valor por paso y paso por ref a const, dependiendo de lo que quiera hacer con los parámetros. En cuanto a las fábricas, la idea es utilizar el reenvío perfecto:
template<class T, class... Args>
unique_ptr<T> make_unique(Args&&... args)
{
T* ptr = new T(std::forward<Args>(args)...);
return unique_ptr<T>(ptr);
}
... y todo está bien. Una regla especial de deducción de argumentos de la plantilla ayuda a diferenciar entre los argumentos lvalue y rvalue, y std :: forward le permite crear expresiones con el mismo valor que tenían los argumentos reales. Entonces, si escribes algo como esto:
string foo();
int main() {
auto ups = make_unique<string>(foo());
}
la cadena que devolvió foo se mueve automáticamente al montón.
Entonces, si lvalues se unía a rvalues a través de una copia implícita, entonces podría escribir solo una sobrecarga como create_object (legacy_string &&, legacy_vector &&, legacy_string &&) y funcionaría más o menos como proporcionar todas las combinaciones de sobrecargas de referencia de valor / valor. ..
Bueno, y sería más o menos equivalente a una función que toma los parámetros por valor. En serio.
¿Es esta una mejora tan significativa que valdría la pena realizar, por ejemplo, un parche experimental para GCC?
No hay mejoría.
No veo tu punto en esta pregunta. Si tiene una clase móvil, solo necesita una versión T
:
struct A {
T t;
A(T t):t(move(t)) { }
};
Y si la clase es tradicional pero tiene un swap
eficiente, puede escribir la versión de intercambio o puede retroceder a la forma const T&
struct A {
T t;
A(T t) { swap(this->t, t); }
};
Con respecto a la versión de intercambio, prefiero ir con const T&
way en lugar de ese intercambio. La principal ventaja de la técnica de intercambio es la seguridad de excepción y es acercar la copia a la persona que llama para que pueda optimizar las copias de los temporales. Pero, ¿qué tienes que guardar si simplemente estás construyendo el objeto de todos modos? Y si el constructor es pequeño, el compilador puede verlo y optimizar las copias también.
struct A {
T t;
A(T const& t):t(t) { }
};
Para mí, no parece correcto convertir automáticamente un valor de cadena en una copia de valor de sí mismo solo para vincularse a una referencia de valor de r. Una referencia rvalue dice que se enlaza a rvalue. Pero si intentas enlazar a un valor l del mismo tipo, mejor falla. La introducción de copias ocultas para permitir que eso no me suene bien, porque cuando la gente ve un X&&
y usted pasa un valor de X
, apuesto a que la mayoría esperará que no haya una copia, y que el enlace es directamente, si es que funciona. Es mejor fallar de inmediato para que el usuario pueda corregir su código.