Смежное хранение полиморфных типов

Мне интересно узнать, существует ли какой-либо жизнеспособный способ смежного хранения массива полиморфных объектов, так что методы virtual на общей базе могут быть юридически вызваны (и отправили бы к правильному переопределенному методу в подклассе).

Например, учитывая следующие классы:

struct B {
  int common;
  int getCommon() { return common; }
  virtual int getVirtual() const = 0;
}

struct D1 : B {
  virtual int getVirtual final const { return 5 };
}

struct D2 : B {
  int d2int;
  virtual int getVirtual final const { return d2int };
}

Я хотел бы выделить смежный массив объектов D1 и D2 и рассматривать их как объекты B, включая вызов getVirtual(), который будет делегировать соответствующий метод в зависимости от типа объекта. Концептуально это кажется возможным: каждый объект знает свой тип, как правило, через встроенный указатель vtable, поэтому вы можете себе представить, сохраняя n объектов в массиве n * max(sizeof(D1), sizeof(D2)) unsigned char и используя размещение new и delete для инициализации объектов и наведение указателя unsigned char на B*. Я уверен, что актерский состав не является законным.

Можно также представить себе создание такого объединения, как:

union Both {
  D1 d1;
  D2 d2;
}

а затем создав массив Both и используя новое место для создания объектов соответствующего типа. Однако, похоже, это еще не дает возможности реально называть B::getVirtual() безопасно. Вы не знаете последний сохраненный тип для элементов, так как вы собираетесь получить B*? Вам нужно использовать либо &u.d1, либо &u.d2, но вы не знаете, какой! Существуют действительно специальные правила о "начальных общих подпоследовательностях", которые позволяют вам делать некоторые вещи в союзах, где элементы имеют общие черты, но этот применим только к стандартным типам макета. Классы с виртуальными методами не являются стандартными типами макетов.

Есть ли способ? В идеальном случае решение будет выглядеть как нерезкий std::vector<B>, который может фактически содержать полиморфные подклассы B. Да, если требуется, можно было бы предусмотреть, что все возможные подклассы известны заранее, но лучшее решение должно знать только максимальный вероятный размер любого подкласса (и не удается во время компиляции, если кто-то пытается добавить "слишком большой" объект).

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

Фон

Без сомнения, кто-то спросит "почему", поэтому здесь немного мотивации:

Как правило, хорошо известно, что использование функций virtual для реализации полиморфизма во время выполнения происходит при умеренных издержках при фактическом вызове виртуальных методов.

Не так часто обсуждается, однако, тот факт, что использование классов с виртуальными методами для реализации полиморфизма обычно подразумевает совершенно другой способ управления памятью для базовых объектов. Вы не можете просто добавлять объекты к стандартным контейнерам разных типов (но общая база): если у вас есть подклассы D1 и D2, оба из базы B, a std::vector<B> будет срезать любой D1 или D2 добавлены объекты. Аналогично для массивов таких объектов.

Обычным решением является использование контейнеров или массивов указателей для базового класса, например std::vector<B*> или, возможно, std::vector<unique_ptr<B>> или std::vector<shared_ptr<B>>. Как минимум, это добавляет дополнительную косвенность при доступе к каждому элементу 1 а в случае с интеллектуальными указателями он разбивает общие оптимизации контейнеров, Если вы фактически распределяете каждый объект через new и delete (включая косвенно), тогда затраты времени и памяти на сохранение ваших объектов просто увеличиваются на большую сумму.

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


1 Помимо всего прочего, это косвенное отношение в значительной степени предотвращает любую векторизация одной и той же операции, применяемой ко всем элементам, и вредит локальности ссылки с последствиями как для кэширования, так и для предварительной выборки.

Ответ 1

Ты был почти со своим союзом. Вы можете использовать либо тегированный союз (добавьте if для распознавания в вашем цикле), либо std::variant (он вводит некоторую двойную диспетчеризацию через std::find, чтобы получить объект из нее) для этого. В любом случае у вас есть распределения по динамическому хранилищу, поэтому гарантируется наличие данных.
В любом случае, как вы можете видеть, в любом случае вы можете заменить дополнительный уровень косвенности (виртуальный вызов) простым прямым вызовом. Вам нужно каким-то образом стереть тип (полиморфизм - это не что иное, как стирание типа, подумайте об этом), и вы не можете выйти прямо из стираемого объекта с помощью простого вызова. if или дополнительные вызовы для заполнения пробела дополнительного уровня косвенности.

Вот пример, который использует std::variant и std::find:

#include<vector>
#include<variant>

struct B { virtual void f() = 0; };
struct D1: B { void f() override {} };
struct D2: B { void f() override {} };

void f(std::vector<std::variant<D1, D2>> &vec) {
    for(auto &&v: vec) {
        std::visit([](B &b) { b.f(); }, v);
    }
}

int main() {
    std::vector<std::variant<D1, D2>> vec;
    vec.push_back(D1{});
    vec.push_back(D2{});
    f(vec);
}

Для этого очень близко, не стоит публиковать также пример, который использует помеченные союзы.


Другой способ сделать это - с помощью отдельных векторов для производных классов и вектора поддержки для их итерации в правильном порядке. Вот минимальный пример, который показывает это:

#include<vector>
#include<functional>

struct B { virtual void f() = 0; };
struct D1: B { void f() override {} };
struct D2: B { void f() override {} };

void f(std::vector<std::reference_wrapper<B>> &vec) {
    for(auto &w: vec) {
        w.get().f();
    }
}

int main() {
    std::vector<std::reference_wrapper<B>> vec;
    std::vector<D1> d1;
    std::vector<D2> d2;

    d1.push_back({});
    vec.push_back(d1.back());
    d2.push_back({});
    vec.push_back(d2.back());

    f(vec);
}

Ответ 2

Я пытаюсь реализовать то, что вам нужно без накладных расходов памяти:

template <typename Base, std::size_t MaxSize, std::size_t MaxAlignment>
struct PolymorphicStorage
{
public:
    template <typename D, typename ...Ts>
    D* emplace(Ts&&... args)
    {
        static_assert(std::is_base_of<Base, D>::value, "Type should inherit from Base");
        auto* d = new (&buffer) D(std::forward<Ts>(args)...);
        assert(&buffer == reinterpret_cast<void*>(static_cast<Base*>(d)));
        return d;
    }

    void destroy() { get().~Base(); }

    const Base& get() const { return *reinterpret_cast<const Base*>(&buffer); }
    Base& get() { return *reinterpret_cast<Base*>(&buffer); }

private:
    std::aligned_storage_t<MaxSize, MaxAlignment> buffer;
};

Демо

Но проблемы в том, что конструкторы (и присвоение) копирования/перемещения неверны, но я не вижу правильного способа реализовать его без накладных расходов памяти (или дополнительного ограничения для класса).

Я не могу =delete их, иначе вы не сможете их использовать в std::vector.

С накладными расходами памяти variant кажется более простым.

Ответ 3

Итак, это действительно уродливо, но если вы не используете множественное наследование или виртуальное наследование, Derived * в большинстве реализаций будет иметь такое же значение уровня бит, как Base *.

Вы можете протестировать это с помощью static_assert, чтобы вещи не скомпилировались, если это не так на конкретной платформе, и используйте вашу идею union.

#include <cstdint>

class Base {
 public:
   virtual bool my_virtual_func() {
      return true;
   }
};

class DerivedA : public Base {
};

class DerivedB : public Base {
};

namespace { // Anonymous namespace to hide all these pointless names.
constexpr DerivedA a;
constexpr const Base *bpa = &a;
constexpr DerivedB b;
constexpr const Base *bpb = &b;

constexpr bool test_my_hack()
{
   using ::std::uintptr_t;

   {
      const uintptr_t dpi = reinterpret_cast<uintptr_t>(&a);
      const uintptr_t bpi = reinterpret_cast<uintptr_t>(bpa);
      static_assert(dpi == bpi, "Base * and Derived * !=");
   }
   {
      const uintptr_t dpi = reinterpret_cast<uintptr_t>(&b);
      const uintptr_t bpi = reinterpret_cast<uintptr_t>(bpb);
      static_assert(dpi == bpi, "Base * and Derived * !=");
   }
   // etc...
   return true;
}
}

const bool will_the_hack_work = test_my_hack();

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

Но, если этот бит кода преуспевает при компиляции, то не имеет значения, получаете ли вы Base * из члена DerivedA или DerivedB вашего объединения. В любом случае они будут одинаковыми.

Другой вариант - вставить указатель на структуру, полную указателей функций-членов в начале структуры, которая содержит этот указатель и объединение с вашими производными классами в нем и инициализировать его самостоятельно. В основном, реализуйте свою собственную таблицу vtable.

Ответ 4

Был разговор на CppCon 2017, "" Полиморфизм времени выполнения - вернуться к основам ", в котором обсуждалось выполнение чего-то вроде того, что вы просите для. Слайды находятся на github, а видео разговора доступно на YouTube.

Экспериментальная библиотека динамиков для достижения этого "dyno" также в github.

Ответ 5

Мне кажется, что вы ищете variant, который является помеченным объединением с безопасным доступом.

С++ 17 имеет std::variant. Для предыдущих версий boost предлагает версию - boost::variant

Обратите внимание, что полиморфизм больше не нужен. В этом случае я использовал совместимые с сигнатурой методы для обеспечения полиморфизма, но вы также можете предоставить его через совместимые с подписью функции и ADL.

#include <variant>   // use boost::variant if you don't have c++17
#include <vector>
#include <algorithm>

struct B {
  int common;
  int getCommon() const { return common; }
};

struct D1 : B {
  int getVirtual() const { return 5; }
};

struct D2 : B {
  int d2int;
  int getVirtual() const { return d2int; }
};

struct d_like
{
    using storage_type = std::variant<D1, D2>;

    int get() const {
        return std::visit([](auto&& b)
        {
            return b.getVirtual();
        }, store_);
    }

    int common() const { 
        return std::visit([](auto&& b)
        {
            return b.getCommon();
        }, store_);
    };

    storage_type store_;
};

bool operator <(const d_like& l, const d_like& r)
{
    return l.get() < r.get();
}

struct by_common
{
    bool operator ()(const d_like& l, const d_like& r) const
    {
        return l.common() < r.common();
    }
};

int main()
{
    std::vector<d_like> vec;
    std::sort(begin(vec), end(vec));
    std::sort(begin(vec), end(vec), by_common());
}