El siguiente código parece bastante inofensivo a primera vista. Un usuario usa la función bar()
para interactuar con alguna funcionalidad de biblioteca. (Esto puede haber funcionado durante mucho tiempo desde que bar()
devolvió una referencia a un valor no temporal o similar). Ahora, sin embargo, simplemente está devolviendo una nueva instancia de B
. B
nuevamente tiene una función a()
que devuelve una referencia a un objeto del tipo iterable A
. El usuario desea consultar este objeto, lo que conduce a una falla predeterminada, ya que el B
objeto temporal devuelto por bar()
se destruye antes de que comience la iteración.
No estoy decidido a quién (biblioteca o usuario) tiene la culpa de esto. Todas las clases proporcionadas por la biblioteca me parecen limpias y ciertamente no están haciendo nada diferente (devolviendo referencias a miembros, devolviendo instancias de pila, ...) que tanto otro código por ahí hace. El usuario no parece hacer nada mal también, solo está iterando sobre algún objeto sin hacer nada relacionado con la vida útil de ese objeto.
(Una pregunta relacionada podría ser: ¿Debería establecerse la regla general de que el código no debe "basarse en el rango para iterar" sobre algo que es recuperado por más de una llamada encadenada en el encabezado del bucle ya que cualquiera de estas llamadas puede devolver un rvalue?)
#include <algorithm>
#include <iostream>
// "Library code"
struct A
{
A():
v{0,1,2}
{
std::cout << "A()" << std::endl;
}
~A()
{
std::cout << "~A()" << std::endl;
}
int * begin()
{
return &v[0];
}
int * end()
{
return &v[3];
}
int v[3];
};
struct B
{
A m_a;
A & a()
{
return m_a;
}
};
B bar()
{
return B();
}
// User code
int main()
{
for( auto i : bar().a() )
{
std::cout << i << std::endl;
}
}
Respuestas:
Creo que el problema fundamental es una combinación de características del lenguaje (o falta de ellas) de C ++. Tanto el código de la biblioteca como el código del cliente son razonables (como lo demuestra el hecho de que el problema está lejos de ser obvio). Si la vida útil de lo temporal
B
se extendiera adecuadamente (hasta el final del ciclo) no habría problema.Hacer que la vida temporal sea lo suficientemente larga y ya no es extremadamente difícil. Ni siquiera un "ad hoc" más bien "todos los temporales involucrados en la creación del rango para un rango basado en vivo hasta el final del ciclo" carecería de efectos secundarios. Considere el caso de
B::a()
devolver un rango que es independiente delB
objeto por valor. Entonces lo temporalB
puede descartarse inmediatamente. Incluso si uno pudiera identificar con precisión los casos en los que es necesaria una extensión de por vida, ya que estos casos no son obvios para los programadores, el efecto (los destructores se llamaron mucho más tarde) sería sorprendente y quizás una fuente igualmente sutil de errores.Sería más deseable detectar y prohibir tales tonterías, forzando al programador a elevarse explícitamente
bar()
a una variable local. Esto no es posible en C ++ 11, y probablemente nunca será posible porque requiere anotaciones. Rust hace esto, donde la firma de.a()
sería:Aquí
'x
hay una variable o región de por vida, que es un nombre simbólico para el período de tiempo que un recurso está disponible. Francamente, las vidas son difíciles de explicar, o aún no hemos descubierto la mejor explicación, por lo que me limitaré al mínimo necesario para este ejemplo y remitiré al lector inclinado a la documentación oficial .El verificador de préstamos se daría cuenta de que el resultado de las
bar().a()
necesidades debe vivir mientras se ejecute el ciclo. Expresarse como una limitación en el tiempo de vida'x
, se escribe:'loop <= 'x
. También se daría cuenta de que el receptor de la llamada al método,bar()
, es temporal. Los dos punteros están asociados con la misma vida útil, por'x <= 'temp
lo tanto, hay otra restricción.¡Estas dos restricciones son contradictorias! Necesitamos
'loop <= 'x <= 'temp
pero'temp <= 'loop
, que captura el problema con bastante precisión. Debido a los requisitos en conflicto, se rechaza el código con errores. Tenga en cuenta que esta es una verificación en tiempo de compilación y el código Rust generalmente resulta en el mismo código de máquina que el código C ++ equivalente, por lo que no necesita pagar un costo de tiempo de ejecución.Sin embargo, esta es una gran característica para agregar a un idioma, y solo funciona si todo el código lo usa. el diseño de las API también se ve afectado (algunos diseños que serían demasiado peligrosos en C ++ se vuelven prácticos, otros no se pueden hacer para jugar bien con las vidas). Por desgracia, eso significa que no es práctico agregar a C ++ (o cualquier lenguaje realmente) de forma retroactiva. En resumen, la culpa está en la inercia que tienen los lenguajes exitosos y el hecho de que Bjarne en 1983 no tuvo la bola de cristal y la previsión para incorporar las lecciones de los últimos 30 años de investigación y experiencia en C ++ ;-)
Por supuesto, eso no es nada útil para evitar el problema en el futuro (a menos que cambie a Rust y nunca vuelva a usar C ++). Se podrían evitar expresiones más largas con múltiples llamadas a métodos encadenados (lo cual es bastante limitante y ni siquiera soluciona remotamente todos los problemas de por vida). O se podría tratar de adoptar una política de propiedad más disciplinada sin la ayuda del compilador: documentar claramente que los
bar
retornos por valor y que el resultadoB::a()
no debe sobrevivirB
a lo quea()
se invoca. Cuando cambie una función para que regrese por valor en lugar de una referencia de mayor duración, tenga en cuenta que se trata de un cambio de contrato . Todavía es propenso a errores, pero puede acelerar el proceso de identificación de la causa cuando sucede.fuente
¿Podemos resolver este problema usando las funciones de C ++?
C ++ 11 ha agregado calificadores de referencia de funciones miembro, lo que permite restringir la categoría de valor de la instancia de clase (expresión) a la que se puede llamar la función miembro. Por ejemplo:
Cuando
begin
llamamos a laend
función miembro, sabemos que lo más probable es que también necesitemos llamar a la función miembro (o algo así comosize
, para obtener el tamaño del rango). Esto requiere que operemos con un valor l, ya que necesitamos abordarlo dos veces. Por lo tanto, puede argumentar que estas funciones miembro deben estar calificadas por lvalue-ref.Sin embargo, esto podría no resolver el problema subyacente: alias. La función
begin
yend
miembro alias el objeto, o los recursos gestionados por el objeto. Si reemplazamosbegin
yend
por una sola funciónrange
, deberíamos proporcionar una que se pueda invocar en valores:Este podría ser un caso de uso válido, pero la definición anterior de
range
no lo permite. Como no podemos abordar el temporal después de la llamada a la función miembro, podría ser más razonable devolver un contenedor, es decir, un rango de propiedad:Aplicando esto al caso del OP, y una ligera revisión del código
Esta función miembro cambia la categoría de valor de la expresión:
B()
es un valor priva, peroB().a()
es un valor l. Por otro lado,B().m_a
es un valor r. Entonces, comencemos haciendo esto consistente. Hay dos maneras de hacer esto:La segunda versión, como se dijo anteriormente, solucionará el problema en el OP.
Además, podemos restringir
B
las funciones miembro de:Esto no tendrá ningún impacto en el código del OP, ya que el resultado de la expresión después del
:
bucle for basado en rango está vinculado a una variable de referencia. Y esta variable (como una expresión utilizada para acceder a sus funcionesbegin
yend
miembros) es un valor l.Por supuesto, la pregunta es si la regla predeterminada debería ser "alias las funciones de los miembros en los valores deberían devolver un objeto que posee todos sus recursos, a menos que haya una buena razón para no hacerlo" . El alias que devuelve se puede usar legalmente, pero es peligroso en la forma en que lo está experimentando: no se puede usar para extender la vida útil de su temporal "principal":
En C ++ 2a, creo que se supone que debe solucionar este problema (o similar) de la siguiente manera:
en lugar de los OP
La solución alternativa especifica manualmente que la vida útil de
b
es el bloque completo del ciclo for.Propuesta que introdujo esta declaración init
Demo en vivo
fuente