c++ - tipos - ¿Por qué la unión no puede ser usada en la herencia?
son tipos de herencia que existen en c++ (3)
Vi la siguiente cosa en el estándar de c ++ (§9.5 / 1):
Un sindicato no tendrá clases de base. Una unión no debe ser usada como una clase base.
Una unión puede tener funciones miembro (incluidos constructores y destructores), pero no funciones virtuales (10.3)
Desde arriba, la unión también puede tener constructor y destructor.
Entonces, ¿por qué no se permite en la herencia?
EDITAR: Para responder a los comentarios:
Si se permite 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, esta manera puede usarse para guardar memoria. Creo que esto es una herencia impropia. ¿Es mejor tener unión dentro de la clase derivada en ese caso?
Si se permite la unión como clase derivada, puede usar servicios de clase base. Por ejemplo, si Union tiene múltiples tipos de datos. Como sabemos, solo se puede utilizar 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, se puede utilizar la herencia múltiple para obtener servicios de todas las clases base para todos los tipos de datos en Union. Esto también me parece un uso inadecuado de la herencia. ¿Pero hay algún concepto equivalente para lograr contenido en este punto?
Solo mis pensamientos ...
(Esta respuesta fue escrita para C ++ 03, la situación puede haber cambiado desde C ++ 11)
No puedo imaginar ninguna razón convincente para excluirla ... más que no hay una razón particularmente buena para incluirla. Los sindicatos simplemente no se usan lo suficiente como para importar mucho, y existen fuertes restricciones sobre el tipo de miembros en el sindicato, restricciones que impiden las prácticas de OO. Esas restricciones, y la carga de rastrear manualmente la (s) variable (s) en particular en una unión que tienen validez, significan que lo primero que quieres hacer con una unión es encapsularla en una clase, luego puedes heredar de eso de todos modos. Permitir que la unión forme parte de la cadena de herencia simplemente extiende el dolor.
Por lo tanto, cualquier cosa que pueda dar a los usuarios casuales de C ++ la impresión de que las uniones podrían mezclarse con su código centrado en OO 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 las variantes de implementación, y tienden a ser un detalle de implementación en lugar de uno de interfaz.
Has agregado un poco a tu pregunta con tu edición ... los pensamientos siguen.
Si se permite 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 puede usarse para guardar memoria. Creo que esto es una herencia impropia. ¿Es mejor tener unión dentro de la clase derivada en ese caso?
Entonces, estamos discutiendo:
union U { int i; double d; };
struct D : U { };
Los miembros de los datos de U deberían ser, implícitamente o explícitamente, públicos, protegidos o privados. Si son públicos o están 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 ser miembros de datos sindicales en una clase.
Si se permite la unión como clase derivada, puede usar servicios de clase base. Por ejemplo, si Union tiene múltiples tipos de datos. Como sabemos, solo se puede utilizar 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, se puede utilizar la herencia múltiple para obtener servicios de todas las clases base para todos los tipos de datos en Union. Esto también me parece un uso inadecuado de la herencia. ¿Pero hay algún concepto equivalente para lograr contenido en este punto?
Ok, aquí es donde las cosas se ponen extrañas. Entonces, tenemos:
union U : public A, private B { };
En primer lugar, déjame 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 no predeterminados, etc. Nota. Estoy hablando de los miembros de datos de la unión, y no de la propia unión. Por lo tanto, A y B serían, si las reglas de contenido de la unión siguen siendo, muy limitadas, y la capacidad de U para derivar no es particularmente útil.
Esto nos lleva a la pregunta de por qué los sindicatos no pueden gestionar 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 se debe construir de forma predeterminada, invocando el destructor primero cuando se asigna un campo de enumeración diferente, etc. ¿Si alguien le hace algo al miembro que no está construido actualmente? ¿Suena bien?
Bueno, en primer lugar, la sobrecarga de una enumeración podría no ser necesaria, ya que el código del cliente podría usar un miembro y luego otro en un orden de apropiación conocido. Los cheques y las excepciones en el idioma en sí son una sobrecarga similar ... pueden ser reducidos a un solo cheque antes de múltiples usos si se dejan al código del cliente. En ambos casos, estaría pagando algunos gastos generales de administración que solo algunas aplicaciones necesitan, un enfoque muy poco basado en C ++.
Ignorando eso, los sindicatos 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ía pensar "¿eh? Los enumas son complejos?", Pero cada enumeración es, conceptualmente generalizada, efectivamente un conjunto arbitrario de campos de bits independientes de ancho variable. No es trivial describir cuáles están destinadas a ser mutuamente excluyentes o dependientes, etc. El compilador no se compromete en absoluto con el espacio del problema. De manera similar, una unión puede tener una o más vistas legítimas al mismo tiempo sobre los mismos datos, mientras que otras son inválidas, con sutilezas para iniciar. Por ejemplo: una unión de int64_t, double y char [4] siempre puede leerse como int64_t o char [4] después de configurarse como doble, pero al revés puede leer un doble no válido y causar un comportamiento indefinido, a menos que está releyendo valores que vinieron de un doble en algún momento anterior, tal vez en una biblioteca de serialización / deserialización. El compilador no quiere participar en la gestión de eso, que es lo que tendría que hacer para garantizar que los miembros objeto de los sindicatos obedecieran las promesas implícitas en su propia encapsulación.
Pregunta: "¿es mejor tener una unión dentro de una clase derivada?" ... no, generalmente no funciona, ya que la mayoría de los objetos no se pueden colocar en una unión (ver arriba). Tiene este problema si la unión está dentro de la clase derivada o, a través de las nuevas características de lenguaje que postula, la unión en realidad es la clase derivada.
Aunque entiendo tu frustración. En la práctica, las personas a veces desean uniones de objetos arbitrarios no triviales, por lo que lo piratean de la manera más dura y dura utilizando reinterpretar cast o similar, administrando la alineación de la memoria de un espacio lo suficientemente grande para el conjunto de objetos que soportará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 los controles de seguridad, etc. no es deducible ni expresable en C ++. El compilador no va a hacer eso por ti.
Aquí hay una solución simple:
struct InheritedUnion
{
union {
type1 member1;
type2 member2;
};
};
struct InheritsUnion : InheritedUnion
{};
Al hacer que el sindicato sea anónimo, funciona como si el tipo de base fuera realmente un sindicato.
La solución de Ben Voigt solo funciona si no necesita heredar miembros de la clase. Si, por algún motivo, necesita varias clases básicas para compartir los mismos datos heredados y también desea heredar las funciones de los miembros, esta es una forma de hacerlo sin ningún costo de 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 puedo decir, esto solo sería útil para unos pocos escenarios de metaprogramación muy específicos, o quizás para alguna programación orientada a aspectos.