2010-09-01 17 views
17

vi debajo cosa en C++ estándar (§9.5/1):¿Por qué la unión no se puede usar en Herencia?

Una unión no tendrán clases base. No se usará una unión como como clase base.

Una unión puede tener funciones miembro (incluyendo constructores y destructores), pero no virtuales (10.3) funciones

Desde arriba, la unión puede tener constructor y el destructor también.

¿Por qué no está permitido en Herencia?

EDIT: Para responder comentarios:

  1. Si se permite que la unión como una clase base, sus datos pueden ser utilizados por una clase derivada. Si la clase derivada está interesada en usar solo un miembro de la unión, de esta manera se puede usar para guardar la memoria. Creo que esta es una herencia impropia. ¿Es mejor tener unión dentro de la clase derivada en ese caso?

  2. Si la unión está permitida como clase derivada, puede usar servicios de clase base. Por ejemplo, si Union tiene múltiples tipos de datos. Como sabemos, solo se puede usar un tipo de datos. Para cada tipo de datos, una clase base está presente para ofrecer servicios para ese tipo en particular. En este caso, la herencia múltiple se puede usar para obtener servicios de todas las clases base para todos los tipos de datos en Union. Esto también lo siento como el uso incorrecto de la herencia. Pero, ¿hay algún concepto equivalente para lograr contenido en este punto?

Sólo mis pensamientos ...

+0

¿Cómo esperaría que un sindicato se comporte si tuviera una clase base? – dan04

+3

¿Qué significaría si fuera posible tener una unión con una clase base o usar una unión como clase base? (La naturaleza de una unión realmente no funciona tan bien con la semántica de la herencia) –

+0

@ dan04, @McNellis: Pls verifique la pregunta actualizada. – bjskishore123

Respuesta

11

(Esta respuesta fue escrito para C++ 03, la situación puede haber cambiado desde C++ 11)

no puedo imaginar cualquier convincente razón para excluirlo ... más que no hay una razón particularmente buena para incluirlo. Los sindicatos simplemente no se usan lo suficiente como para importar tanto, y existen fuertes restricciones sobre el tipo de miembros en la unión, restricciones que impiden las prácticas de OO. Esas restricciones, y la carga de hacer un seguimiento manual de las variables particulares en una unión que tienen validez, significan que casi lo primero que quieres hacer con una unión es encapsularla en una clase, luego puedes heredarla de todos modos. Permitir que la unión sea parte de la cadena de herencia simplemente expande el dolor.

Por lo tanto, cualquier cosa que pueda dar a los usuarios ocasionales de C++ la impresión de que las uniones pueden mezclarse en su código OO-centrado causaría más problemas que beneficios. Los sindicatos son básicamente un truco para la conservación de la memoria, la reinterpretación de datos rápida pero desagradable y la implementación de variantes, y tienden a ser un detalle de implementación en lugar de uno de interfaz.

Agregaste un poco a tu pregunta con tu edición ... los pensamientos siguen.

Si se permite la unión como clase base, sus datos pueden ser utilizados por una clase derivada. Si la clase derivada está interesada en usar solo un miembro de la unión, de esta manera se puede usar para guardar la memoria. Creo que esta es una herencia impropia. ¿Es mejor tener unión dentro de la clase derivada en ese caso?

Por lo tanto, estamos hablando:

union U { int i; double d; }; 
struct D : U { }; 

miembros de datos de U serían o bien tienen que ser - implícita o explícitamente - público, protegido o privado. Si son públicos o protegidos, entonces no están encapsulados, y estamos nuevamente en el escenario de "compartir el dolor" mencionado anteriormente. U tiene menos capacidad para encapsularlos que una clase que contiene una unión. Si son privados, entonces podrían fácilmente ser un miembro de datos sindicales en una clase.

Si la unión está permitida como clase derivada, puede usar servicios de clase base. Por ejemplo, si Union tiene múltiples tipos de datos. Como sabemos, solo se puede usar un tipo de datos. Para cada tipo de datos, una clase base está presente para ofrecer servicios para ese tipo en particular. En este caso, la herencia múltiple se puede usar para obtener servicios de todas las clases base para todos los tipos de datos en Union. Esto también lo siento como el uso incorrecto de la herencia. Pero, ¿hay algún concepto equivalente para lograr contenido en este punto?

Ok, aquí es donde las cosas se ponen raras. Entonces, tenemos:

union U : public A, private B { }; 

En primer lugar, permítanme ser un poco más explícito sobre algo. Los sindicatos no pueden contener objetos complejos y encapsulados; son la antítesis de la encapsulación. Está bastante limitado a los datos POD, y no puede tener constructores que no sean predeterminados, etc. Tenga en cuenta que estoy hablando de los miembros de datos de la unión, y no de la unión misma. Entonces, A y B tendrían -si esas reglas de contenido sindical permanecen- serían muy limitadas, y la habilidad de U para derivar no es particularmente útil.

Esto lleva a la pregunta de por qué los sindicatos no pueden administrar objetos más complejos de una manera segura. Bueno, ¿cómo podrían hacerlo? La respuesta obvia es agregar una enumeración oculta para decir cuál es válida, y algunas reglas sobre qué tipo deben construirse por defecto, invocando primero el destructor cuando se asigna un campo de enumeración diferente, etc. etc ... Tal vez deberían arrojar si alguien le hace algo al miembro que no está actualmente construido? ¿Suena bien?

Bueno, en primer lugar, la sobrecarga de una enumeración puede no ser necesaria, ya que el código del cliente puede usar un miembro y luego otro en un orden conocido. Los cheques y las excepciones en el idioma en sí son una sobrecarga similar ... pueden ser reducidos a una sola verificación antes de usos múltiples si se los deja al código del cliente. En ambos casos, estarías pagando por una sobrecarga de administración que solo necesitan algunas aplicaciones, un enfoque muy poco común en C++.

Ignorando eso, las uniones no son tan simples de todos modos. Al igual que las enumeraciones, su uso está diseñado para ser flexible y sería difícil comunicarse con el compilador para que pueda limpiarlo, verificarlo y automatizarlo. Podrías pensar "¿eh? Enums son complejos?", Pero cada enumeración es, conceptualmente generalizada, efectivamente un conjunto arbitrario de bitfields independientes de ancho variable. No es trivial describir cuáles deben ser mutuamente excluyentes o dependientes, etc. El compilador no compra nada en el espacio del problema. Del mismo modo, una unión puede tener una o más vistas simultáneas legítimas sobre los mismos datos, mientras que otras son inválidas, con sutilezas para arrancar. Por ejemplo: una unión de int64_t, double y char [4] siempre podría leerse como int64_t o char [4] después de establecerse como un doble, pero a la inversa podría leer un doble no válido y causar un comportamiento indefinido, a menos que está volviendo a leer los valores que provienen de un doble en algún momento anterior, tal vez en una biblioteca de serialización/deserialización. El compilador no quiere comprar en la gestión de eso, que es lo que debería hacer para asegurar que los miembros de los sindicatos obedezcan las promesas implícitas en su propia encapsulación.

Usted pregunta "¿es mejor tener unión dentro de la clase derivada?" ...no, generalmente no funciona, ya que la mayoría de los objetos no se pueden unir (ver arriba). Se topa con este problema si la unión está dentro de la clase derivada o, a través de las nuevas características del lenguaje que postula, la unión en realidad es la clase derivada.

Sin embargo, entiendo su frustración. En la práctica, las personas a veces quieren uniones de objetos arbitrarios no triviales, por lo que lo piratean de manera dura y sucia utilizando reinterpretación o similar, gestionando la alineación de memoria de un espacio lo suficientemente grande para el conjunto de objetos que admitirán (o - más fácil pero más lento - un puntero a ellos). Encontrará este tipo de cosas en la variante boost y en cualquier biblioteca. Pero no puede derivar de ellos ... el conocimiento sobre el uso apropiado, el alcance de las verificaciones de seguridad, etc. simplemente no es deducible ni expresable en C++. El compilador no va a hacer eso por ti.

14

Aquí es una solución simple:

struct InheritedUnion 
{ 
    union { 
     type1 member1; 
     type2 member2; 
    }; 
}; 

struct InheritsUnion : InheritedUnion 
{}; 

Al hacer la unión anónima, funciona igual que si el tipo de base eran en realidad una unión.

+0

Quería una solución y una justificación de por qué no debería usarse en herencia. +1 por la mitad de la respuesta. – bjskishore123

0

La solución de Ben Voigt solo funciona si no necesita heredar los miembros de la clase. Si por alguna razón necesita varias clases de base de compartir los mismos datos heredados, y también quieren heredar funciones miembro, aquí hay una manera de hacerlo sin ningún coste espacio/tiempo adicional:

#include <iostream> 
#include <type_traits> 

namespace detail { 

    // If you use Boost, you can use use boost::copy_cv instead 
    template<class T, class U> 
    struct copy_cv { 

     using const_copied = std::conditional_t< 
      std::is_const<U>{}, std::add_const_t<T>, T>; 

     using type = std::conditional_t< 
      std::is_volatile<U>{}, 
      std::add_volatile_t<const_copied>, 
      const_copied>; 
    }; 

    // a derived class uses this template to share data between bases 
    template<typename Data, typename Derived> 
    struct storage { 

     template<typename BasePtr> 
     static inline constexpr decltype(auto) 
     get(BasePtr&& ptr) { 

      // enforcing cv-qualifiers from the BasePtr 
      using qualified_base = 
       std::remove_reference_t<std::remove_pointer_t<BasePtr>>; 

      using qualified_derived = 
       typename copy_cv<Derived, qualified_base>::type; 

      using qualified_data = 
       typename copy_cv<Data, qualified_base>::type; 

      // casting the base "this" pointer to the base class with data 
      return static_cast<qualified_data*>(static_cast<qualified_derived*>(ptr)); 
     } 
    }; 
} 

// the base class templates here ending with "_impl" have no data, and are 
// EBO-ed away. They ultimately uses data from a different base class 
template<typename Data> 
struct print_impl { 

    void print() const { 
     std::cout << Data::get(this)->number << '\n'; 
    } 
}; 

// add_impl_1 and add_impl_2 supply "overloaded" member functions for the derived class 
template<typename Data> 
struct add_impl_1 { 

    int add(int i) const { 
     return Data::get(this)->number + i; 
    } 
}; 

template<typename Data> 
struct add_impl_2 { 

    template<int i> 
    int add() const { 
     return Data::get(this)->number + i; 
    } 
}; 

// this is the base class containing data 
struct bar_data { 

    int number = 42; 
}; 

struct bar : 

    // derived class inherits the data class 
    bar_data, 

    // using the storage template, we give the "implementation" 
    // base classes access to the data 
    print_impl<detail::storage<bar_data, bar>>, 
    add_impl_1<detail::storage<bar_data, bar>>, 
    add_impl_2<detail::storage<bar_data, bar>> { 

    // using declarations are necessary to disambiguate the "overloads" 
    using add_impl_1<detail::storage<bar_data, bar>>::add; 
    using add_impl_2<detail::storage<bar_data, bar>>::add; 
}; 

static_assert(sizeof(bar_data) == sizeof(bar), ""); 

int main() { 

    bar b{}; 

    b.print(); 
    std::cout << b.add(1) << std::endl; 
    std::cout << b.add<2>() << std::endl; 
} 

Por lo que puede contar, esto solo sería útil para algunos escenarios de metaprogramación muy específicos, o quizás alguna programación orientada a aspectos.

Cuestiones relacionadas