Estoy revisando un código como este, donde A es un tipo móvil:

// Returns true exactly when ownership of a is taken
bool MaybeConsume(A&& a) {
  if (some condition) {
    Consume(std::move(a));  // ???
    return true;
  }
  return false;
}

// ... elsewhere ...

A a;
if (!MaybeConsume(std::move(a))) {
  a.DoSomething();  // !!!
}

Nuestra herramienta de análisis estático se queja de que a se usa después de ser movido (en !!!). IIUC std::move es solo un static_cast, y el objeto a no se destripará hasta que se llame a un constructor de movimientos o un operador de asignación (presumiblemente en Consume). Suponiendo que MaybeConsume satisface el contrato en el comentario,

  1. ¿Esto funciona?
  2. ¿Es UB?
  3. ¿std::move en ??? no tiene operación?

(Probablemente esta instancia en particular pueda refactorizarse para evitar la sutileza, pero aún me gustaría pedir mi propia comprensión).

23
stewbasic 15 feb. 2018 a las 07:32

2 respuestas

La mejor respuesta

Es una advertencia falsa de su herramienta de análisis estático.

  1. ¿Funciona?

Sí, MaybeConsume está haciendo lo que dice el comentario. Solo se está apropiando de su argumento cuando some condition es verdadero (asumiendo que Consume realmente mueve la construcción / asignación de su argumento).

std::move es realmente una fantasía static_cast<T&&> así que { {X2}} no está transfiriendo la propiedad, simplemente está vinculando una referencia al parámetro de MaybeConsume.

  1. ¿Es UB?

No, no está utilizando a si MaybeConsume indica que ha asumido la propiedad de su argumento.

  1. ¿std::move en ??? no tiene operación?

Bueno, no es una operación porque es solo un static_cast, pero si quería preguntar si es innecesario, entonces no, no lo es. Dentro del cuerpo de MaybeConsume, a es un lvalue porque tiene un nombre. Si la firma de Consume es void Consume(A&&), entonces el código no se compilará sin ese std::move.


A partir del uso de ejemplo que ha mostrado, parece que no se supone que debe llamar a MaybeConsume con un argumento prvalue, ya que la persona que llama debería usar el argumento de alguna otra manera si la función devuelve false. Si eso es cierto, entonces debería cambiar su firma a bool MaybeConsume(A&). Esto probablemente hará feliz a su herramienta de análisis estático porque le permitirá escribir if (!MaybeConsume(a)).

14
Praetorian 15 feb. 2018 a las 05:32

Para comprender por qué la herramienta de análisis estático genera una advertencia, es necesario pensar en la forma en que lo hace un analizador estático. Cuando ve un fragmento de código como el siguiente:

A a;
fun(std::move(a);
a.method();

No está claro qué podría suceder dentro de la llamada fun (). Para ejecutar con éxito el método () en un depende que se cumplan algunos requisitos previos, que pueden no (o ya no) se cumplen después de la llamada de fun (). Si bien el programador bien puede saber que es seguro llamar al método (), el analizador no, por lo que genera una advertencia.

La siguiente es solo mi propia opinión. Es más seguro simplemente asumir que la propiedad de a está totalmente tomada por diversión (). Para evitar confusiones, es mejor imponer un estilo de pedir prestado y devolver. Pensando en ello como si un amigo te pidiera prestado un libro, no puedes (no puedes) usar ese libro hasta que te lo devuelvan. Por lo tanto, nunca se arriesgue a invocar accidentalmente un objeto que debería estar "muerto" para entonces.

Vea el código de demostración a continuación:

#include <iostream>
#include <utility>
#include <tuple>
#include<cassert>
struct A {
public:
    int *p; 
public:
    A() {
        p = new int();
        assert(p != nullptr);
        std::cout << p << std::endl;
        std::cout << "default constrctor is called" << std::endl;
    }
    A(const A&) = delete;
    A& operator=(const A&) = delete;
    A(A&& _a): p(_a.p) {
        _a.p = nullptr;
        std::cout << p << std::endl;
        std::cout << "move constructor is called" << std::endl;;
    }
    A& operator=(A&& _a) {
        std::cout << "move assignment is called"<<std::endl;;
        p = std::move(_a.p);
        return *this;
    }
    void DoSomthing(){
        std::cout << "do somthing is called" << std::endl;
        *p = 100;
        std::cout << "value of p is changed"<<std::endl;
    }
};
std::tuple<A&&, bool>  MaybeConsume(A&& a) {
    if (1==2) {//try 1==1 alternatively
        delete a.p;
        a.p = nullptr;//consume
        std::cout << "content consumed" << std::endl;
        return std::make_tuple(Consume(std::move(a)), true);
    }
    else {
        return std::make_tuple(std::move(a), false);
    }
}

int main()
{
    A a;
    std::tuple<A&&, bool> t = MaybeConsume(std::move(a));
    if (!(std::get<bool> (t))) {
        A a1 = std::move(std::get<A&&>(t));
        a1.DoSomthing();
    }
    return 0;
}
0
John Z. Li 15 feb. 2018 a las 09:54