2009-12-04 20 views
54

¿Alguna vez tiene sentido comprobar si este es nulo?Comprobando si es nulo

Digamos que tengo una clase con un método; dentro de ese método, marque this == NULL, y si lo es, devuelva un código de error.

Si este es nulo, entonces eso significa que se elimina el objeto. ¿El método puede devolver algo?

Actualización: me olvidó mencionar que el método puede ser llamado desde varios subprocesos y puede causar que se elimina el objeto mientras que otro hilo está dentro del método.

+4

No significa que el objeto fue eliminado. Eliminar un puntero no lo pone a cero automáticamente, y '((Foo *) 0) -> foo()' es una sintaxis perfectamente válida. Siempre y cuando 'foo()' no sea una función virtual, esto funcionará incluso en la mayoría de los compiladores, pero es simplemente asqueroso. –

+9

"puede causar que el objeto se elimine mientras otro hilo está dentro del método". Esto no es aceptable, no debe eliminar un objeto mientras que otro código (en el mismo subproceso o uno diferente) conserva una referencia que usará. Tampoco hará que 'this' se vuelva nulo en ese otro hilo. –

+0

Estoy viendo una situación en este momento donde definitivamente es NULL. Dios solo sabe por qué. – Owl

Respuesta

67

¿Alguna vez tiene sentido comprobar esto == nulo? Encontré esto mientras hacía una revisión del código.

En C++ estándar, no lo hace, porque cualquier llamada en un puntero nulo es ya un comportamiento indefinido, por lo que cualquier código de depender de tales controles no es estándar (no hay garantía de que el cheque incluso será ejecutado).

Tenga en cuenta que esto también se aplica a las funciones no virtuales.

Algunas implementaciones permiten this==0, sin embargo, y en consecuencia las bibliotecas escritas específicamente para esas implementaciones algunas veces lo usarán como un hack. Un buen ejemplo de dicho par es VC++ y MFC: no recuerdo el código exacto, pero recuerdo claramente haber visto if (this == NULL) verificaciones en el código fuente de MFC en alguna parte.

También puede estar allí como una ayuda para la depuración, porque en algún momento en el pasado este código fue golpeado con this==0 debido a un error en la persona que llama, por lo que se insertó un cheque para coger las futuras instancias de ese. Sin embargo, una afirmación tendría más sentido para tales cosas.

Si esto == null significa que el objeto se ha eliminado.

No, no significa eso. Significa que se llamó a un método en un puntero nulo, o en una referencia obtenida de un puntero nulo (aunque obtener dicha referencia ya es U.B.). Esto no tiene nada que ver con delete, y no requiere que ningún objeto de este tipo haya existido alguna vez.

+7

Con 'delete', lo opuesto suele ser cierto: si' eliminas 'un objeto, no se establece en NULL, y luego intentas invocar un método en él, encontrarás que 'this! = NULL', pero probablemente se bloqueará o se comportará de forma extraña si su memoria ya ha sido recuperada para ser utilizada por algún otro objeto. –

+1

IIRC, el estándar permite que 'delete' establezca el puntero a NULL, pero no lo requiere, y en la práctica casi ninguna implementación lo hace. – rmeador

+4

'delete' puede no obtener siempre un l-value, considere' eliminar esto + 0; ' – GManNickG

24

Su nota sobre los hilos es preocupante. Estoy bastante seguro de que tienes una condición de carrera que puede provocar un accidente. Si un hilo elimina un objeto y lo pone a cero, otro hilo podría hacer una llamada a través de ese puntero entre esas dos operaciones, lo que llevaría al this a ser no nulo y tampoco válido, lo que provocaría un bloqueo. Del mismo modo, si un hilo llama a un método mientras otro hilo está en el medio de crear el objeto, también puede obtener un bloqueo.

Respuesta breve, realmente necesita utilizar un mutex o algo para sincronizar el acceso a esta variable. Debe asegurarse de que this es nunca nulo o tendrá problemas.

+5

"Debe asegurarse de que esto nunca sea nulo" - Creo que un mejor enfoque es asegurar que el operando izquierdo de 'operator->' nunca sea nulo :) pero aparte de eso, desearía poder +10 esto. –

6

FWIW, he usado comprobaciones de depuración para (this != NULL) en aserciones anteriores que han ayudado a detectar código defectuoso. No es que el código necesariamente haya llegado demasiado lejos sin un bloqueo, pero en sistemas integrados pequeños que no tienen protección de memoria, las afirmaciones en realidad ayudaron.

En los sistemas con protección de memoria, el sistema operativo generalmente alcanzará una infracción de acceso si se llama con un puntero NULL this, por lo que hay menos valor para afirmar this != NULL. Sin embargo, consulte el comentario de Pavel sobre por qué no es necesariamente inútil incluso en sistemas protegidos.

+1

Todavía hay un caso para afirmaciones, AV o no. El problema es que un AV por lo general no sucederá hasta que una función miembro realmente intente acceder a un campo miembro. Muy a menudo simplemente llaman a algo más (etc.), hasta que eventualmente algo se estrella en la línea. Alternativamente, pueden llamar a un miembro de otra clase o una función global, y pasar el (supuestamente no nulo) 'this' como argumento. –

+0

@Pavel: es cierto: he suavizado mi redacción sobre el valor de afirmar 'this' en sistemas con protección de memoria. –

+0

en realidad, si assert va a funcionar, entonces definitivamente el código principal también funcionará ¿no? – Gokul

-1

También agregaría que, por lo general, es mejor evitar valores nulos o NULL. Creo que el estándar está cambiando una vez más aquí, pero por ahora 0 es realmente lo que desea comprobar para estar absolutamente seguro de que está obteniendo lo que desea.

+0

La comprobación de NULL no hace tal cosa. –

0

Es muy probable que su método (puede variar de un compilador) pueda ejecutarse y también que pueda devolver un valor. Siempre que no acceda a ninguna variable de instancia. Si lo intenta, se bloqueará.

Como otros señalaron, no puede usar esta prueba para ver si un objeto ha sido eliminado. Incluso si pudiera, no funcionaría, porque el objeto puede ser eliminado por otro hilo justo después de la prueba, pero antes de ejecutar la siguiente línea después de la prueba. Use la sincronización de subprocesos en su lugar.

Si this es nulo, hay un error en su programa, muy probablemente en el diseño de su programa.

-1

Esto es solo un puntero pasado como el primer argumento de una función (que es exactamente lo que lo convierte en un método). Mientras no esté hablando de métodos virtuales y/o herencia virtual, entonces sí, puede encontrarse ejecutando un método de instancia, con una instancia nula. Como otros dijeron, es casi seguro que no llegarás muy lejos con esa ejecución antes de que surjan problemas, pero la codificación robusta probablemente debería verificar esa situación, con una afirmación. Por lo menos, tiene sentido cuando se sospecha que podría estar ocurriendo por alguna razón, pero hay que rastrear exactamente pila qué clase/llamada que está ocurriendo en.

-1

este NULL == podría ser útil tener un comportamiento de reserva (por ejemplo, un mecanismo de delegación de asignador opcional que retrocedería a malloc/free). No estoy seguro de su estándar, pero si NUNCA llama a ninguna función virtual y, por supuesto, no tiene acceso de miembro dentro del método en esa rama, no hay motivo real para bloquear. De lo contrario, tiene un compilador muy pobre que utiliza un puntero de función virtual cuando no necesita serlo, y luego, antes de cumplir estrictamente con el estándar, es mejor que cambie su compilador.

En algún momento es útil porque la lectura y la refactorización pueden, en unos pocos casos, ganar el estándar (cuando obviamente no es un comportamiento indefinido para todos los compiladores existentes).

Sobre el artículo: "Todavía comparando" este "Puntero a nulo" quizás la razón es que el autor del artículo tiene menos comprensión de lo que hace un compilador que el equipo de software de Microsoft que escribió el MFC.

+0

"tiene un compilador muy pobre que utiliza el puntero de función virtual cuando no es necesario que lo esté, y antes de cumplir estrictamente con el estándar, es mejor que cambie su compilador". - LOL, no. Esto no tiene nada que ver con los virtuales y todo tiene que ver con las malas prácticas de codificación con UB. Arregle su código y use un compilador que advierta, lo que le permite evitar el código que habilita la depuración totalmente innecesaria o agujeros de seguridad más adelante. Además: "cuando su comportamiento obviamente no es indefinido para todos los compiladores existentes" El estándar dicta UB, no algún compilador. Se debe evitar confiar en una implementación particular de UB. –

4

Sé que esto es viejo pero siento que ahora que estamos lidiando con C++ 11-17, alguien debería mencionar a las lambdas. Si captura esto en una lambda que se llamará asincrónicamente en un momento posterior en el tiempo, es posible que su objeto "this" se destruya antes de invocar a esa lambda.

es decir, consumiendo como una devolución de llamada a alguna función en tiempo caros que se ejecuta desde un hilo separado o simplemente de forma asíncrona en general

EDIT: Para que quede claro, la pregunta era "¿Alguna vez tiene sentido para comprobar si esto es nulo "Simplemente estoy ofreciendo un escenario en el que tiene sentido que podría ser más frecuente con el uso más amplio de C++ moderno.

Ejemplo de código auxiliar: Este código es completamente ejecutable. Para ver el comportamiento inseguro, simplemente comente la llamada al comportamiento seguro y elimine el comentario de la llamada de comportamiento inseguro.

#include <memory> 
#include <functional> 
#include <iostream> 
#include <future> 

class SomeAPI 
{ 
public: 
    SomeAPI() = default; 

    void DoWork(std::function<void(int)> cb) 
    { 
     DoAsync(cb); 
    } 

private: 
    void DoAsync(std::function<void(int)> cb) 
    { 
     std::cout << "SomeAPI about to do async work\n"; 
     m_future = std::async(std::launch::async, [](auto cb) 
     { 
      std::cout << "Async thread sleeping 10 seconds (Doing work).\n"; 
      std::this_thread::sleep_for(std::chrono::seconds{ 10 }); 
      // Do a bunch of work and set a status indicating success or failure. 
      // Assume 0 is success. 
      int status = 0; 
      std::cout << "Executing callback.\n"; 
      cb(status); 
      std::cout << "Callback Executed.\n"; 
     }, cb); 
    }; 
    std::future<void> m_future; 
}; 

class SomeOtherClass 
{ 
public: 
    void SetSuccess(int success) { m_success = success; } 
private: 
    bool m_success = false; 
}; 
class SomeClass : public std::enable_shared_from_this<SomeClass> 
{ 
public: 
    SomeClass(SomeAPI* api) 
     : m_api(api) 
    { 
    } 

    void DoWorkUnsafe() 
    { 
     std::cout << "DoWorkUnsafe about to pass callback to async executer.\n"; 
     // Call DoWork on the API. 
     // DoWork takes some time. 
     // When DoWork is finished, it calls the callback that we sent in. 
     m_api->DoWork([this](int status) 
     { 
      // Undefined behavior 
      m_value = 17; 
      // Crash 
      m_data->SetSuccess(true); 
      ReportSuccess(); 
     }); 
    } 

    void DoWorkSafe() 
    { 
     // Create a weak point from a shared pointer to this. 
     std::weak_ptr<SomeClass> this_ = shared_from_this(); 
     std::cout << "DoWorkSafe about to pass callback to async executer.\n"; 
     // Capture the weak pointer. 
     m_api->DoWork([this_](int status) 
     { 
      // Test the weak pointer. 
      if (auto sp = this_.lock()) 
      { 
       std::cout << "Async work finished.\n"; 
       // If its good, then we are still alive and safe to execute on this. 
       sp->m_value = 17; 
       sp->m_data->SetSuccess(true); 
       sp->ReportSuccess(); 
      } 
     }); 
    } 
private: 
    void ReportSuccess() 
    { 
     // Tell everyone who cares that a thing has succeeded. 
    }; 

    SomeAPI* m_api; 
    std::shared_ptr<SomeOtherClass> m_data = std::shared_ptr<SomeOtherClass>(); 
    int m_value; 
}; 

int main() 
{ 
    std::shared_ptr<SomeAPI> api = std::make_shared<SomeAPI>(); 
    std::shared_ptr<SomeClass> someClass = std::make_shared<SomeClass>(api.get()); 

    someClass->DoWorkSafe(); 

    // Comment out the above line and uncomment the below line 
    // to see the unsafe behavior. 
    //someClass->DoWorkUnsafe(); 

    std::cout << "Deleting someClass\n"; 
    someClass.reset(); 

    std::cout << "Main thread sleeping for 20 seconds.\n"; 
    std::this_thread::sleep_for(std::chrono::seconds{ 20 }); 

    return 0; 
} 
+2

Pero incluso si el objeto se destruye, ¿no terminará la lambda con un puntero "this" capturado no nulo? – interfect

+3

¡Exactamente! Comprobar si "this" == nullptr o NULL no será suficiente en este caso ya que "this" estará colgando. Simplemente lo mencioné porque algunos eran escépticos de que tal semántica incluso necesitara existir. –

+0

No creo que esta respuesta sea relevante ya que la pregunta solo se refiere a "Digamos que tengo una clase con un método, dentro de ese método". Lambdas es solo un ejemplo de donde puedes obtener un puntero colgante. – user2672165

0

Sé que esto es una cuestión de edad, sin embargo pensé que voy a compartir mi experiencia con el uso de la captura Lambda

faltas segmento
#include <iostream> 
#include <memory> 

using std::unique_ptr; 
using std::make_unique; 
using std::cout; 
using std::endl; 

class foo { 
public: 
    foo(int no) : no_(no) { 

    } 

    template <typename Lambda> 
    void lambda_func(Lambda&& l) { 
     cout << "No is " << no_ << endl; 
     l(); 
    } 

private: 
    int no_; 
}; 

int main() { 
    auto f = std::make_unique<foo>(10); 

    f->lambda_func([f = std::move(f)]() mutable { 
     cout << "lambda ==> " << endl; 
     cout << "lambda <== " << endl; 
    }); 

    return 0; 
} 

este código

$ g++ -std=c++14 uniqueptr.cpp 
$ ./a.out 
Segmentation fault (core dumped) 

Si quito la std::cout instrucción de lambda_func El código se ejecuta hasta su finalización.

Parece que esta instrucción f->lambda_func([f = std::move(f)]() mutable { procesa las capturas lambda antes de invocar la función de miembro.

Cuestiones relacionadas