Подтвердить что ты не робот

Почему профсоюз нельзя использовать в Наследии?

Я видел ниже в стандарте С++ (§9.5/1):

Союз не должен иметь базовые классы. Союз не должен используется как базовый класс.

Объединение может иметь функции-члены (включая конструкторы и деструкторы), но не виртуальные (10.3) Функции

Сверху объединение также может иметь конструктор и деструктор.

Так почему это не допустимо в наследовании?

EDIT: для ответа на комментарии:

  • Если объединение разрешено в качестве базового класса, его данные могут использоваться производным классом. Если производному классу интересно использовать только один член объединения, этот способ можно использовать для сохранения памяти. Я думаю, это ненадлежащее наследование. В этом случае лучше иметь союз внутри производного класса?

  • Если union разрешен как производный класс, он может использовать службы базового класса. Например, если Union имеет несколько типов данных. Как известно, можно использовать только один тип данных. Для каждого типа данных существует базовый класс для предоставления услуг для этого конкретного типа. В этом случае множественное наследование может использоваться для получения услуг всех базовых классов для всех типов данных в Союзе. Это также я чувствую как ненадлежащее использование наследования. Но есть ли эквивалентная концепция для достижения контента в этой точке?

Просто мои мысли...

4b9b3361

Ответ 1

(Этот ответ был написан для С++ 03, ситуация, возможно, изменилась с С++ 11)

Я не могу представить никаких веских оснований для его исключения... более того, нет особо веских оснований для его включения. Союзы просто не используются настолько, чтобы иметь такое значение, и существуют серьезные ограничения на тип членов профсоюза, ограничения, препятствующие практике ОО. Эти ограничения - и бремя ручного отслеживания конкретной переменной (ов) в союзе, имеющей правомерность - означают в значительной степени первое, что вы хотите сделать с объединением, инкапсулировать его в класс, а затем вы можете наследовать от этого. Предоставление объединению в цепочку наследования просто распространяется вокруг.

Итак, все, что может дать случайным пользователям С++ впечатление, что союзы могут быть смешаны с их OO-ориентированным кодом, вызовет больше проблем, чем пользы. Союзы в основном являются взломом для сохранения памяти, быстрой, но неприятной интерпретации данных и вариантов реализации, и, как правило, являются деталями реализации, а не интерфейсом.

Вы добавили немного к своему вопросу с вашими изменениями... мысли следуют.

Если объединение разрешено в качестве базового класса, его данные могут использоваться производным классом. Если производному классу интересно использовать только один член объединения, этот способ можно использовать для сохранения памяти. Я думаю, это ненадлежащее наследование. В этом случае лучше иметь союз внутри производного класса?

Итак, мы обсуждаем:

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

Члены данных U будут либо должны быть - неявно или явно - общедоступными, защищенными или частными. Если они публичные или защищенные, то они не инкапсулированы, и мы вернулись в сценарий "поделиться болью", упомянутый выше. U обладает меньшей способностью инкапсулировать их, чем класс, содержащий объединение. Если они являются частными, то они могут так же легко быть членом данных объединения в классе.

Если union разрешен как производный класс, он может использовать службы базового класса. Например, если Union имеет несколько типов данных. Как известно, можно использовать только один тип данных. Для каждого типа данных существует базовый класс для предоставления услуг для этого конкретного типа. В этом случае множественное наследование может использоваться для получения услуг всех базовых классов для всех типов данных в Союзе. Это также я чувствую как ненадлежащее использование наследования. Но есть ли эквивалентная концепция для достижения контента в этой точке?

Хорошо, здесь все странно. Итак, у нас есть:

union U : public A, private B { };

Во-первых, позвольте мне немного более подробно остановиться на чем-то. Союзы не могут содержать сложные, инкапсулированные объекты - они являются антитезой инкапсуляции. Вы в значительной степени ограничены данными POD и не можете иметь конструкторы не по умолчанию и т.д. Примечание. Я говорю о членах данных объединения, а не о самом союзе. Таким образом, A и B будут - если эти правила союза-содержания останутся - будут очень ограниченными, а способность U получить не особенно полезна.

Это приводит к вопросу о том, почему профсоюзы не могут безопасно управлять более сложными объектами. Ну, как они могли это сделать? Очевидным ответом является добавление скрытого перечисления, чтобы сказать, какой из них действителен, и некоторые правила о том, какой тип должен быть сконструирован по умолчанию, вызывая деструктор сначала, когда назначено другое поле перечисления, и т.д. И т.д. И т.д. Может быть, они должны бросить если кто-то что-то делает для члена, который в настоящее время не сконструирован? Звучит нормально?

Ну, во-первых, накладные расходы перечисления могут не потребоваться, так как клиентский код может использовать один член, а затем другой в известном-appopriate порядке. Проверки и исключения на языке сами по себе являются подобными накладными расходами... они могут быть учтены до одной проверки перед несколькими использованиями, если оставить код клиента. В обоих случаях вы будете платить за некоторые издержки управления, которые нужны только некоторым приложениям - очень не-С++-подход.

Игнорируя это, профсоюзы в любом случае не так просты. Как и перечисления, их использование призвано быть гибким, и было бы сложно связаться с компилятором, чтобы он мог очищать, проверять и автоматизировать его. Вы могли бы подумать, что "эйнусы сложны?", Но каждое перечисление - концептуально обобщенное - фактически произвольное множество независимых битовых полей различной ширины. Это нетривиально, чтобы описать, какие из них должны быть взаимоисключающими или зависимыми и т.д. Компилятор вообще не покупает проблемное пространство. Точно так же союз может иметь одно или несколько одновременно легитимных представлений по тем же данным, в то время как другие являются недействительными, с тонкостями для загрузки. Например: объединение int64_t, double и char [4] всегда может быть прочитано как int64_t или char [4] после того, как установлено как double, но наоборот может прочитать недопустимый двойник и вызвать undefined, если вы не перечитываете значения, полученные из двойника в более раннее время, возможно, в библиотеке сериализации/десериализации. Компилятор не хочет покупать управление этим, что должно было бы сделать, чтобы гарантировать, что члены объектов союзов подчиняются promises, неявному в их собственной инкапсуляции.

Вы спрашиваете: "Лучше ли иметь союз внутри производного класса?"... нет - обычно не работает, поскольку большинство объектов нельзя вставлять в объединение (см. выше). Вы сталкиваетесь с этой проблемой, является ли объединение внутри производного класса, или - через новые функции языка, которые вы постулируете, - объединение фактически является производным классом.

Я понимаю ваше разочарование. На практике люди иногда хотят объединения произвольных нетривиальных объектов, поэтому они взломают его жестким и грязным способом, используя реинтерпрет-листинг или подобное, управляя выравниванием памяти некоторого пространства, достаточно большого для набора объектов, которые они будут поддерживать (или - проще, но медленнее - указатель на них). Вы найдете такие вещи в варианте boost и в любых библиотеках. Но вы не можете извлечь из них... знание о надлежащем использовании, степень проверок безопасности и т.д. Просто не выводимо или не выражено в С++. Компилятор не собирается делать это за вас.

Ответ 2

Вот простой способ:

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

struct InheritsUnion : InheritedUnion
{};

Сделав объединение анонимным, он работает так же, как если бы базовый тип был действительно объединением.

Ответ 3

Обходное решение Ben Voigt работает только в том случае, если вам не нужно наследовать членов класса. Если по какой-то причине вам нужны несколько базовых классов для совместного использования одних и тех же унаследованных данных, а также хотите наследовать функции-члены, здесь можно сделать это без каких-либо дополнительных затрат пространства/времени:

#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;
}

Насколько я могу судить, это было бы полезно только для нескольких очень специфических сценариев метапрограммирования или, возможно, для аспектно-ориентированного программирования.