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

Ad hoc-полиморфизм и гетерогенные контейнеры со значениями семантики

У меня есть ряд несвязанных типов, которые поддерживают одни и те же операции через перегруженные свободные функции (ad hoc polymorphism):

struct A {};

void use(int x) { std::cout << "int = " << x << std::endl; }
void use(const std::string& x) { std::cout << "string = " << x << std::endl; }
void use(const A&) { std::cout << "class A" << std::endl; }

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

std::vector<???> items;
items.emplace_back(3);
items.emplace_back(std::string{ "hello" });
items.emplace_back(A{});

for (const auto& item: items)
    use(item);
// or better yet
use(items);

И, конечно, это должно быть полностью расширяемо. Подумайте о библиотечном API, который принимает vector<???> и клиентский код, который добавляет свои собственные типы к уже известным.


Обычное решение - хранить (умные) указатели на (абстрактный) интерфейс (например, vector<unique_ptr<IUsable>>), но у него есть ряд недостатков - от верхней части головы:

  • Мне нужно перенести мою текущую стратегическую полиморфную модель в иерархию классов, где каждый отдельный класс наследуется от общего интерфейса. О, хвати! Теперь мне приходится писать обертки для int и string, а что нет... Не говоря уже о уменьшении возможности повторного использования/компоновки из-за того, что свободные функции-члены тесно связаны с интерфейсом (виртуальные функции-члены).
  • Контейнер теряет семантику значения: простое присваивание vec1 = vec2 невозможно, если мы используем unique_ptr (заставляя меня вручную выполнять глубокие копии), или оба контейнера заканчиваются с общим состоянием, если мы используем shared_ptr (который имеет свои преимущества и недостатки - но поскольку я хочу семантику ценности на контейнере, снова я вынужден вручную выполнять глубокие копии).
  • Чтобы иметь возможность выполнять глубокие копии, интерфейс должен поддерживать виртуальную функцию clone(), которая должна быть реализована в каждом отдельном производном классе. Можете ли вы серьезно подумать о чем-то более скучном, чем это?

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


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

В идеале я хотел бы иметь простой vector<IUsable> с правильной семантикой значений, не меняя ничего на мою текущую иерархию типов (отсутствия) и сохраняя ad hoc-полиморфизм вместо требуемого полиморфизма подтипа.

Возможно ли это? Если да, то как?

4b9b3361

Ответ 1

Различные альтернативы

Это возможно. Существует несколько альтернативных подходов к вашей проблеме. У каждого есть разные преимущества и недостатки (я объясню каждый):

  • Создайте интерфейс и получите класс шаблонов, который реализует этот интерфейс для разных типов. Он должен поддерживать клонирование.
  • Используйте boost::variant и посетив.

Смешение статического и динамического полиморфизма

Для первой альтернативы вам необходимо создать такой интерфейс:

class UsableInterface 
{
public:
    virtual ~UsableInterface() {}
    virtual void use() = 0;
    virtual std::unique_ptr<UsableInterface> clone() const = 0;
};

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

template <typename T> class UsableImpl : public UsableInterface
{
public:
    template <typename ...Ts> UsableImpl( Ts&&...ts ) 
        : t( std::forward<Ts>(ts)... ) {}
    virtual void use() override { use( t ); }
    virtual std::unique_ptr<UsableInterface> clone() const override
    {
        return std::make_unique<UsableImpl<T>>( t ); // This is C++14
        // This is the C++11 way to do it:
        // return std::unique_ptr<UsableImpl<T> >( new UsableImpl<T>(t) ); 
    }

private:
    T t;
};

Теперь вы уже можете делать все, что вам нужно. Вы можете поместить эти вещи в вектор:

std::vector<std::unique_ptr<UsableInterface>> usables;
// fill it

И вы можете скопировать этот вектор, сохраняющий базовые типы:

std::vector<std::unique_ptr<UsableInterface>> copies;
std::transform( begin(usables), end(usables), back_inserter(copies), 
    []( const std::unique_ptr<UsableInterface> & p )
    { return p->clone(); } );

Вы, вероятно, не захотите засорять свой код такими вещами. Вы хотите написать

copies = usables;

Ну, вы можете получить это удобство, обернув std::unique_ptr в класс, который поддерживает копирование.

class Usable
{
public:
    template <typename T> Usable( T t )
        : p( std::make_unique<UsableImpl<T>>( std::move(t) ) ) {}
    Usable( const Usable & other ) 
        : p( other.clone() ) {}
    Usable( Usable && other ) noexcept 
        : p( std::move(other.p) ) {}
    void swap( Usable & other ) noexcept 
        { p.swap(other.p); }
    Usable & operator=( Usable other ) 
        { swap(other); }
    void use()
        { p->use(); }
private:
    std::unique_ptr<UsableInterface> p;
};

Из-за красивого шаблонного конструктора вы можете теперь писать такие вещи, как

Usable u1 = 5;
Usable u2 = std::string("Hello usable!");

И вы можете назначить значения с правильной семантикой значений:

u1 = u2;

И вы можете поместить использование в std::vector

std::vector<Usable> usables;
usables.emplace_back( std::string("Hello!") );
usables.emplace_back( 42 );

и скопируйте этот вектор

const auto copies = usables;

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

Кроме того, вы можете использовать более общий подход, чем писать собственный класс Usable и пересылать все функции-члены (если вы хотите добавить другое позже). Идея состоит в том, чтобы заменить класс Usable на класс шаблона. Этот класс шаблонов не будет предоставлять функцию-член use(), но operator T&() и operator const T&() const. Это дает вам ту же функциональность, но вам не нужно писать дополнительный класс значений каждый раз, когда вы облегчаете этот шаблон.

Безопасный, общий, изолированный контейнер на основе стека

класс шаблона boost::variant - это именно то, что обеспечивает C-стиль union, но безопасный и с правильной семантикой значений. Способ его использования таков:

using Usable = boost::variant<int,std::string,A>;
Usable usable;

Вы можете назначить из объектов любого из этих типов Usable.

usable = 1;
usable = "Hello variant!";
usable = A();

Если все типы шаблонов имеют семантику значений, то boost::variant также имеет семантику значений и может быть помещен в контейнеры STL. Вы можете написать функцию use() для такого объекта с помощью шаблона, который называется шаблоном . Он вызывает правильную функцию use() для содержащегося объекта в зависимости от внутреннего типа.

class UseVisitor : public boost::static_visitor<void>
{
public:
    template <typename T>
    void operator()( T && t )
    {
        use( std::forward<T>(t) );
    }
}

void use( const Usable & u )
{
    boost::apply_visitor( UseVisitor(), u );
}

Теперь вы можете написать

Usable u = "Hello";
use( u );

И, как я уже упоминал, вы можете помещать эти штучки в контейнеры STL.

std::vector<Usable> usables;
usables.emplace_back( 5 );
usables.emplace_back( "Hello world!" );
const auto copies = usables;

Компромиссы

Вы можете расширить функциональность в двух измерениях:

  • Добавить новые классы, которые удовлетворяют статическому интерфейсу.
  • Добавить новые функции, которые должны реализовать классы.

В первом подходе, который я представил, легче добавлять новые классы. Второй подход упрощает добавление новых функций.

В первом подходе это невозможно (или, по крайней мере, трудно) для клиентского кода для добавления новых функций. Во втором подходе невозможно (или, по крайней мере, трудно) для клиентского кода добавлять новые классы в микс. Выход - так называемый ациклический шаблон посетителя, который позволяет клиентам расширять иерархию классов новыми классами и новыми функциями. Недостатком здесь является то, что вам приходится жертвовать определенной статичной проверкой во время компиляции. Здесь ссылка которая описывает шаблон посетителя, включая ациклический шаблон посетителя наряду с некоторыми другими альтернативами. Если у вас есть вопросы по этому поводу, я готов ответить.

Оба подхода супер безопасны. Там нет компромиссов.

Затраты времени выполнения первого подхода могут быть намного выше, поскольку для каждого создаваемого вами элемента имеется распределение кучи. Подход boost::variant основан на стеках и, следовательно, быстрее. Если производительность является проблемой с первым подходом, рассмотрите возможность переключения на второй.

Ответ 2

Кредит, в котором это произошло: Когда я смотрел Шон Родитель, начинающий родной 2013 "Наследование - это базовый класс зла" , я понял, понял насколько просто на самом деле было, в ретроспективе, решить эту проблему. Я могу только посоветовать вам посмотреть его (там гораздо больше интересного материала, упакованного всего за 20 минут, этот Q/A едва царапает поверхность всего разговора), а также другие разговоры Going Native 2013.


На самом деле это так просто, что вряд ли нужно какое-либо объяснение, код говорит сам за себя:

struct IUsable {
  template<typename T>
  IUsable(T value) : m_intf{ new Impl<T>(std::move(value)) } {}
  IUsable(IUsable&&) noexcept = default;
  IUsable(const IUsable& other) : m_intf{ other.m_intf->clone() } {}
  IUsable& operator =(IUsable&&) noexcept = default;
  IUsable& operator =(const IUsable& other) { m_intf = other.m_intf->clone(); return *this; }

  // actual interface
  friend void use(const IUsable&);

private:
  struct Intf {
    virtual ~Intf() = default;
    virtual std::unique_ptr<Intf> clone() const = 0;
    // actual interface
    virtual void intf_use() const = 0;
  };
  template<typename T>
  struct Impl : Intf {
    Impl(T&& value) : m_value(std::move(value)) {}
    virtual std::unique_ptr<Intf> clone() const override { return std::unique_ptr<Intf>{ new Impl<T>(*this) }; }
    // actual interface
    void intf_use() const override { use(m_value); }
  private:
    T m_value;
  };
  std::unique_ptr<Intf> m_intf;
};

// ad hoc polymorphic interface
void use(const IUsable& intf) { intf.m_intf->intf_use(); }

// could be further generalized for any container but, hey, you get the drift
template<typename... Args>
void use(const std::vector<IUsable, Args...>& c) {
  std::cout << "vector<IUsable>" << std::endl;
  for (const auto& i: c) use(i);
  std::cout << "End of vector" << std::endl;
}

int main() {
  std::vector<IUsable> items;
  items.emplace_back(3);
  items.emplace_back(std::string{ "world" });
  items.emplace_back(items); // copy "items" in its current state
  items[0] = std::string{ "hello" };
  items[1] = 42;
  items.emplace_back(A{});
  use(items);
}

// vector<IUsable>
// string = hello
// int = 42
// vector<IUsable>
// int = 3
// string = world
// End of vector
// class A
// End of vector

Как вы можете видеть, это довольно простая оболочка вокруг unique_ptr<Interface>, с шаблоном конструктором, который создает производную Implementation<T>. Все (не совсем) детали gory являются частными, открытый интерфейс не может быть чище: сама оболочка не имеет функций-членов, кроме построения/копирования/перемещения, интерфейс предоставляется как бесплатная функция use(), которая перегружает существующие из них.

Очевидно, что выбор unique_ptr означает, что нам нужно реализовать частную функцию clone(), которая вызывается всякий раз, когда мы хотим сделать копию объекта IUsable (что, в свою очередь, требует выделения кучи). Разумеется, одно распределение кучи на один экземпляр довольно субоптимально, но это требование, если какая-либо функция открытого интерфейса может мутировать базовый объект (т.е. Если use() принимает неконстантные ссылки и модифицирует их): таким образом мы гарантируем, что каждый объект уникален и поэтому может свободно мутироваться.


Теперь, если, как и в вопросе, объекты полностью неизменяемы (не только через открытый интерфейс, заметьте, я действительно имею в виду все объекты всегда и полностью неизменны), то мы можем ввести разделенное состояние без гнусных побочных эффектов. Самый простой способ сделать это - использовать shared_ptr - для-const вместо unique_ptr:

struct IUsableImmutable {
  template<typename T>
  IUsableImmutable(T value) : m_intf(std::make_shared<const Impl<T>>(std::move(value))) {}
  IUsableImmutable(IUsableImmutable&&) noexcept = default;
  IUsableImmutable(const IUsableImmutable&) noexcept = default;
  IUsableImmutable& operator =(IUsableImmutable&&) noexcept = default;
  IUsableImmutable& operator =(const IUsableImmutable&) noexcept = default;

  // actual interface
  friend void use(const IUsableImmutable&);

private:
  struct Intf {
    virtual ~Intf() = default;
    // actual interface
    virtual void intf_use() const = 0;
  };
  template<typename T>
  struct Impl : Intf {
    Impl(T&& value) : m_value(std::move(value)) {}
    // actual interface
    void intf_use() const override { use(m_value); }
  private:
    const T m_value;
  };
  std::shared_ptr<const Intf> m_intf;
};

// ad hoc polymorphic interface
void use(const IUsableImmutable& intf) { intf.m_intf->intf_use(); }

// could be further generalized for any container but, hey, you get the drift
template<typename... Args>
void use(const std::vector<IUsableImmutable, Args...>& c) {
  std::cout << "vector<IUsableImmutable>" << std::endl;
  for (const auto& i: c) use(i);
  std::cout << "End of vector" << std::endl;
}

Обратите внимание, что функция clone() исчезла (нам она больше не нужна, мы просто разделяем базовый объект, и это не беспокоит, поскольку оно неизменно), и как теперь копировать noexcept благодаря shared_ptr гарантии.

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

  std::vector<IUsableImmutable> items;
  items.emplace_back(3);
  items[0] = std::string{ "hello" };

(только мутация shared_ptr мутируется, а не основной объект, поэтому он не влияет на другие общие ссылки)

Ответ 3

Может быть, boost:: variant?

#include <iostream>
#include <string>
#include <vector>
#include "boost/variant.hpp"

struct A {};

void use(int x) { std::cout << "int = " << x << std::endl; }
void use(const std::string& x) { std::cout << "string = " << x << std::endl; }
void use(const A&) { std::cout << "class A" << std::endl; }

typedef boost::variant<int,std::string,A> m_types;

class use_func : public boost::static_visitor<>
{
public:
    template <typename T>
    void operator()( T & operand ) const
    {
        use(operand);
    }
};
int main()
{
    std::vector<m_types> vec;
    vec.push_back(1);
    vec.push_back(2);
    vec.push_back(std::string("hello"));
    vec.push_back(A());
    for (int i=0;i<4;++i)
        boost::apply_visitor( use_func(), vec[i] );
    return 0;
}

Пример в реальном времени: http://coliru.stacked-crooked.com/a/e4f4ccf6d7e6d9d8

Ответ 4

Другие ответы ранее (используйте базовый класс интерфейса vtabled, используйте boost:: variant, используйте приемы наследования на основе виртуального базового класса) - все это очень хорошие и допустимые решения для этой проблемы, каждый из которых имеет разностный баланс времени компиляции и времени выполнения, Я бы предположил, что вместо boost:: variant, на С++ 11 и более поздних версиях использовать egg:: variant вместо, который является повторной реализацией boost:: вариант с использованием С++ 11/14, и он чрезвычайно превосходит по дизайну, производительности, простоте использования, мощности абстракции и даже обеспечивает довольно полное подмножество функций на VS2013 (и полный набор функций на VS2015). Он также написан и поддерживается ведущим автором Boost.

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

newtype newcontainer = oldcontainer.push_back (newitem);

Это было больно использовать в С++ 03, хотя Boost.Fusion делает честный кусок, чтобы сделать их потенциально полезными. Фактически полезная удобство использования возможно только с С++ 11 и далее, начиная с С++ 14, благодаря родовым лямбдам, которые делают работу с этими гетерогенными коллекциями очень простой для программирования с использованием функционального программирования constexpr, и, вероятно, нынешняя ведущая библиотека инструментальных средств для этого прямо сейчас предложил Boost.Hana, который в идеале требует clang 3.6 или GCC 5.0.

Контейнеры гетерогенных типов в значительной степени составляют 99% времени компиляции 1% времени исполнения. Вы увидите множество оптимизаторов для оптимизатора компилятора с текущей технологией компилятора, например. Я однажды увидел, что clang 3.5 генерирует 2500 кодов кода для кода, которые должны были сгенерировать два кода операций, а для того же кода GCC 4.9 выплюнул 15 опкодов, 12 из которых фактически ничего не делали (они загружали память в регистры и ничего не делали с этими регистрами), Все, что сказало, через несколько лет вы сможете добиться оптимальной генерации кода для контейнеров с гетерогенным типом, и в этот момент я бы ожидал, что они станут следующей генной формой метапрограммирования С++, где вместо arsing around с шаблонами мы будем иметь возможность функционально программировать компилятор С++ с использованием реальных функций!!!

Ответ 5

Вот идея, которую я недавно получил от реализации std::function в libstdС++:

Создайте класс шаблона Handler<T> со статической функцией , которая знает, как копировать, удалять и выполнять другие операции над T.

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

Примерно так:

struct Foo { ... }
struct Bar { ... }
struct Baz { ... }

template<class T>
struct Handler
{
    static void action(Ptr data, EActions eAction)
    {
       switch (eAction)
       {
       case COPY:
           call T::T(...);

       case DELETE:
           call T::~T();

       case OTHER:
           call T::whatever();
       }
    }
}

struct Any
{
    Ptr handler;
    Ptr data;

    template<class T>
    Any(T t)
      : handler(Handler<T>::action)
      , data(handler(t, COPY))
    {}

    Any(const Any& that)
       : handler(that.handler)
       , data(handler(that.data, COPY))
    {}

    ~Any()
    {
       handler(data, DELETE);
    }
};

int main()
{
    vector<Any> V;

    Foo foo; Bar bar; Baz baz;

    v.push_back(foo);
    v.push_back(bar);
    v.push_back(baz);
}

Это дает вам стирание типа при сохранении семантики значений и не требует модификации содержащихся классов (Foo, Bar, Baz) и вообще не использует динамический полиморфизм. Это довольно классный материал.