2012-09-22 17 views
13

Según cppreference.com, std::shared_ptr proporciona un conjunto completo de operadores relativos (==,! =, <, ...), pero la semántica de la comparación no se especifica. Supongo que comparan los punteros crudos subyacentes con los objetos a los que se hace referencia, y que std :: weak_ptr y std :: unique_ptr hacen lo mismo.¿Está bien heredar de los punteros inteligentes C++ 11 y anular los operadores relativos?

Para algunos propósitos, preferiría tener operadores relativos que pidan los punteros inteligentes basados ​​en la comparación de los objetos referenciados (en lugar de los punteros a ellos). Esto ya es algo que hago mucho, pero con mis propios "consejos tontos" que se comportan principalmente como indicadores crudos, excepto para los operadores relativos. Me gustaría hacer lo mismo con los punteros inteligentes estándar C++ 11 también. Así que ...

  1. ¿Está bien que heredar de los C++ 11 punteros inteligentes (shared_ptr, weak_ptr y unique_ptr) y anular los operadores relativos?

  2. ¿Hay algún problema furtivo que deba tener en cuenta? Por ejemplo, ¿hay algún otro método que necesite implementar o usar using para garantizar que todo funcione correctamente?

  3. Para lo último en pereza, ¿hay una plantilla de biblioteca disponible que haga esto automáticamente?

Espero que este sea un "¡por supuesto que puedes hacer eso, idiota!" tipo de cosas, pero estoy un poco inseguro porque hay algunas clases en la biblioteca estándar (contenedores como std::map al menos) que se supone que no debe heredar.

+3

En general, no es seguro heredar de algo que destructor no sea dinámico. Puede ser y se hace comúnmente, solo tienes que ser muy cuidadoso. –

+0

@Mooing Duck - OK, no hay enlace tardío, lo que podría ser un problema para el destructor y en cualquier otro lugar - tiene sentido. No creo que esto sea un problema para mí, al menos en este momento, pero tendré que verificarlo. Tal vez sería mejor incluir un puntero inteligente como miembro en lugar de heredarlo. – Steve314

+1

Er, por "dinámico", por supuesto, quiero decir "virtual" –

Respuesta

8

En general, no es seguro heredar de algo que destructor no sea dinámico. Puede hacerse y se hace comúnmente, solo debes ser muy cuidadoso. En lugar de heredar de los punteros, usaría la composición, especialmente dado que el número de miembros es relativamente pequeño. Usted puede ser capaz de hacer una clase de plantilla para este

template<class pointer_type> 
class relative_ptr { 
public: 
    typedef typename std::pointer_traits<pointer_type>::pointer pointer; 
    typedef typename std::pointer_traits<pointer_type>::element_type element_type; 
    relative_ptr():ptr() {} 
    template<class U> 
    relative_ptr(U&& u):ptr(std::forward<U>(u)) {} 
    relative_ptr(relative_ptr<pointer>&& rhs):ptr(std::move(rhs.ptr)) {} 
    relative_ptr(const relative_ptr<pointer>& rhs):ptr(std::move(rhs.ptr)) {} 

    void swap (relative_ptr<pointer>& rhs) {ptr.swap(rhs.ptr);} 
    pointer release() {return ptr.release();} 
    void reset(pointer p = pointer()) {ptr.reset(p);} 
    pointer get() const {return ptr.get();} 
    element_type& operator*() const {return *ptr;} 
    const pointer_type& operator->() const {return ptr;} 

    friend bool operator< (const relative_ptr& khs, const relative_ptr& rhs) const 
    {return std::less<element>(*lhs,*rhs);} 
    friend bool operator<=(const relative_ptr& khs, const relative_ptr& rhs) const 
    {return std::less_equal<element>(*lhs,*rhs);} 
    friend bool operator> (const relative_ptr& khs, const relative_ptr& rhs) const 
    {return std::greater<element>(*lhs,*rhs);} 
    friend bool operator>=(const relative_ptr& khs, const relative_ptr& rhs) const 
    {return std::greater_equal<element>(*lhs,*rhs);} 
    friend bool operator==(const relative_ptr& khs, const relative_ptr& rhs) const 
    {return *lhs==*rhs;} 
    friend bool operator!=(const relative_ptr& khs, const relative_ptr& rhs) const 
    {return *lhs!=*rhs;} 
protected: 
    pointer_type ptr; 
}; 

Obviamente, la simplicidad de la envoltura se reduce a un mínimo común denominador de los punteros inteligentes, pero lo que sea. No son exactamente complicados, podrías hacer uno para cada clase de puntero inteligente.

Voy a dar una advertencia de que no me gusta la forma en que == funciona, ya que puede devolver cierto para dos punteros a diferentes objetos. Pero lo que sea. Tampoco probé el código, podría fallar en ciertas tareas, como intentar copiar cuando contiene un único_ptr.

+0

Encendido "ya que puede ser cierto para dos punteros a objetos diferentes" - sí, pero lo mismo se aplica a las instancias normales. Piense en estos como proxies que también resultan ser indicadores inteligentes. - Ser un puntero es más un detalle de implementación que la abstracción proporcionada. – Steve314

+0

Debería haber dicho esto antes, pero también hay un precedente en C++ para punteros que actúan como proxies y no necesitan ser desreferenciados - las referencias '' '' También se llama punteros de autorreferencia. – Steve314

+0

Un poco más intrincado de lo que realmente se necesita para este caso de uso ... :) –

3

Es peligroso heredar de cualquier clase que admita asignación y copia-construcción, debido al riesgo de cortar una instancia de clase derivada a la mitad al asignarla accidentalmente a una variable de clase base. Esto afecta a la mayoría de las clases, y es prácticamente imposible de evitar, por lo que se requiere vigilancia por parte de los usuarios de la clase siempre que copien las instancias.

Debido a esto, las clases destinadas a funcionar como bases generalmente no deberían ser compatibles con la copia. Cuando es necesario copiar, deberían proporcionar algo como Derived* clone() const override en su lugar.

El problema que está tratando de resolver probablemente se resuelva dejando las cosas como están y proporcionando comparadores personalizados cuando se trabaja con dichos punteros.

std::vector<std::shared_ptr<int>> ii = …; 
std::sort(begin(ii), end(ii), 
      [](const std::shared_ptr<int>& a, const std::shared_ptr<int>& b) { 
       return *a < *b; 
      }); 
8

Lo primero, como otros ya han señalado es que la herencia no es el camino a seguir. Pero en lugar de la envoltura enrevesado sugerida por la respuesta aceptada, haría algo mucho más simple: poner en práctica su propia comparador para sus propios tipos:

namespace myns { 
struct mytype { 
    int value; 
}; 
bool operator<(mytype const& lhs, mytype const& rhs) { 
    return lhs.value < rhs.value; 
} 
bool operator<(std::shared_ptr<mytype> const & lhs, std::shared_ptr<mytype> const & rhs) 
{ 
    // Handle the possibility that the pointers might be NULL!!! 
    // ... then ... 
    return *lhs < *rhs; 
} 
} 

El magia, que no es realmente mágico en absoluto es el argumento Dependiente Búsqueda (aka. Koening Lookup o ADL). Cuando el compilador encuentra una llamada de función, agregará el espacio de nombres de los argumentos para buscar. Si los objetos son la instanciación de una plantilla, el compilador también agregará los espacios de nombres de los tipos utilizados para crear una instancia de la plantilla. Así, en:

int main() { 
    std::shared_ptr<myns::mytype> a, b; 
    if (a < b) {      // [1] 
     std::cout << "less\n"; 
    } else { 
     std::cout << "more\n"; 
    } 
} 

En [1], y debido a a y b son objetos tipos definidos por el usuario(*) ADL entrará en funcionamiento y que se sumará tanto std y myns al conjunto de las operaciones de búsqueda. A continuación, se encuentra la definición estándar de operator< para std::shared_ptr que es:

template<class T, class U> 
bool std::operator<(shared_ptr<T> const& a, shared_ptr<U> const& b) noexcept; 

Y también añadirá myns y añade:

bool myns::operator<(mytype const& lhs, mytype const& rhs); 

Entonces, una vez finalizada la búsqueda, la resolución de sobrecarga entra en acción, y determinará que myns::operator< es una coincidencia mejor que std::operator< para la llamada, ya que es una combinación perfecta y en ese caso, las no plantillas tienen preferencia. Luego llamará a su propio operator< en lugar del estándar.

Esto se vuelve un poco más intrincado si su tipo es realmente una plantilla, si lo es, suelte un comentario y ampliaré la respuesta.


(*) Se trata de una ligera simplificación. Debido a que operator< se puede implementar como una función de miembro o una función libre, el compilador comprobará dentro de std::shared_ptr<> para el miembro operator< (no presente en el estándar) y amigos. También mirará dentro de mytype para las funciones friend ... y así sucesivamente. Pero al final encontrará el correcto.

+0

Hago esto cuando es apropiado pero, como señalé en varios comentarios, eso es más cosas que pasar y posiblemente mezclar. Cuando sepa que es correcto usar las dos cosas juntas, empaquetarlas juntas asegura que viajen juntas, y el tipado estático proporciona controles adicionales. Y si bien el nombre "puntero" puede ser engañoso, esto es más un tipo de proxy, independientemente de la falta de problemas de red, eso no significa que empaquetar la funcionalidad de esa manera sea algo malo. – Steve314

+0

@ Steve314 Realmente no entiendo lo que quieres decir en tu comentario. Las funciones gratuitas en el espacio de nombres del tipo son parte de la misma * interfaz * (en C++ la interfaz de un objeto no está limitada a sus miembros, debido a ADL), y esta solución en realidad la está explotando. Solo necesita definir en el mismo espacio de nombres como su tipo (y por lo tanto en la misma * interfaz *) su función libre 'operator <'. Muchas personas se confunden con OO, lo que significa que solo puede usar funciones miembro, pero C++ es un lenguaje más rico que eso, y este es uno de los casos en los que brilla, puede ampliar la biblioteca. –

+2

No hay * nada * para * pasar * en esta solución. Los operadores están definidos * una vez * en el espacio de nombres de su tipo, y están disponibles * en todas partes *, * implícitamente *, sin ningún código adicional en el lugar de la llamada. No puede ser más simple que esto. –

Cuestiones relacionadas