У меня есть несколько несвязанных типов, которые поддерживают одни и те же операции через перегруженные свободные функции (специальный полиморфизм):
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>
с правильной семантикой значения, ничего не меняя к моей текущей (отсутствие) иерархии типов, и сохранить специальный полиморфизм вместо того, чтобы требовать полиморфизма подтипа.
Это возможно? Если так, то как?
Это возможно. Есть несколько альтернативных подходов к вашей проблеме. У каждого есть свои преимущества и недостатки (я объясню каждый):
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;
И вы можете поместить Usables в 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
подход основан на стеке и поэтому, вероятно, быстрее. Если с первым подходом возникает проблема производительности, подумайте о переходе на второй.
Кредит, где он должен: Когда я смотрел Шон Родитель Going Native 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
—к сопзЬ вместо 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
мутирует, а не сам базовый объект, поэтому он не влияет на другие общие ссылки)
Может быть, Boost :: вариант?
#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
Другие ответы ранее (используйте базовый класс интерфейса vtabled, используйте boost :: option, используйте приемы наследования виртуального базового класса) — все они являются отличными и правильными решениями для этой проблемы, каждый из которых имеет разное соотношение времени компиляции и затрат времени выполнения. Я хотел бы предложить, однако, что вместо Boost :: Вариант, на C ++ 11 и позже используйте яйца :: вариант вместо который является повторной реализацией boost :: Вариант с использованием C ++ 11/14, и он чрезвычайно превосходит по дизайну, производительности, простоте использования, мощности абстракции и даже обеспечивает довольно полный поднабор функций на VS2013 (и полный набор функций на VS2015 ). Он также написан и поддерживается ведущим автором Boost.
Если вам удастся немного переопределить проблему — в частности, что вы можете потерять стирающий тип std :: vector в пользу чего-то более мощного — вы можете вместо этого использовать контейнеры разнородного типа. Они работают, возвращая новый тип контейнера для каждой модификации контейнера, поэтому шаблон должен быть:
newtype newcontainer = oldcontainer.push_back (newitem);
Это было неудобно в C ++ 03, хотя Boost.Fusion делает все возможное, чтобы сделать их потенциально полезными. Фактически полезное удобство использования возможно только с C ++ 11 и далее, особенно с C ++ 14 и далее благодаря универсальным лямбдам, которые делают работу с этими гетерогенными коллекциями очень простой для программирования с использованием функционального программирования constexpr, и, вероятно, текущая ведущая библиотека инструментальных средств для этого сейчас предложил Boost.Hana который в идеале требует clang 3.6 или GCC 5.0.
Контейнеры гетерогенного типа — это решение, основанное на 99% времени компиляции и 1% времени выполнения. Вы увидите множество заводов, оптимизирующих компилятор, с современной технологией компилятора, например, Однажды я видел, как clang 3.5 генерирует 2500 кодов операций для кода, который должен был генерировать два кода операции, и для того же кода GCC 4.9 выплюнул 15 кодов операций, 12 из которых фактически ничего не делали (они загружали память в регистры и ничего не делали с этими регистрами) , Все это говорит о том, что через несколько лет вы сможете добиться оптимальной генерации кода для контейнеров разнородного типа, и в этот момент я ожидаю, что они станут новой формой метапрограммирования C ++, где вместо возни с шаблонами мы будем уметь функционально программировать компилятор C ++ с использованием актуальных функций !!!
Вот идея, которую я недавно получил от std::function
реализация в libstdc ++:
Создать Handler<T>
шаблон класса с статическая функция-член который знает, как копировать, удалять и выполнять другие операции на T.
Затем сохраните указатель функции на эту статическую функцию в конструкторе вашего класса Any. Вашему классу 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) и вообще не использует динамический полиморфизм. Это довольно крутые вещи.