#c #c 11
#c #c 11
Вопрос:
Допустим, у меня есть универсальный класс Container, который содержит кортеж любого типа и имеет функцию template<typename T> Tamp; get<T>();
, которая возвращает ссылку на элемент в кортеже. Моя очень простая реализация выглядит следующим образом:
template<typename... Ts>
class Container
{
std::tuple<Ts...> contents;
public:
Container(const Tsamp;... ts) : contents(ts...) {}
template <typename T>
Tamp; get()
{
//TypeIndex is some meta-programming struct to find index of T in Ts
return std::get<TypeIndex<T, Ts...>::value>(contents);
}
};
Существуют ли какие-либо хорошие методы удаления типа, позволяющие превратить контейнер в обычный класс без изменения сигнатуры функции get? Как при вызове get<T>()
без знания полного списка типов кортежей? Что-то вроде этого:
Struct A { int x; }
Struct B { int y; }
Struct C { int z; }
int main()
{
Container container(A(), B()); //Underlying storage is a std::tuple<A, B>
Aamp; a = container.get<A>(); //Doesn't know the tuples type list but assumes A is in there.
Camp; c = container.get<C>(); //C isn't in the tuples type list, crash program, which would be correct behavior.
}
boost::any
это обычное решение для решения проблем такого типа, но не решает эту конкретную проблему, потому что мне нужно было бы знать фактический тип базового кортежа для приведения. Например, если бы я попытался использовать это в примере выше, я бы сделал boost::any_cast<std::tuple<A, B>>
, чтобы получить A или B, которые мне бесполезны, потому что я намеренно пытаюсь скрыть список типов кортежей.
Редактировать: полное определение TypeIndex.
#include <type_traits>
template <typename T, typename... Ts>
struct TypeIndex;
template <typename T, typename... Ts>
struct TypeIndex<T, T, Ts...> : std::integral_constant<std::size_t, 0> {};
template <typename T, typename U, typename... Ts>
struct TypeIndex<T, U, Ts...> : std::integral_constant<std::size_t, 1 TypeIndex<T, Ts...>::value> {};
Комментарии:
1. Прав ли я, предполагая,
TypeIndex
что функция определена вами? Этоconstexpr
?2. Да, TypeIndex — это определенная мной структура, производная от
std::intergral_constant
, которая получает индекс типа в списке типов, это вспомогательная структура дляstd::get<N>();
3. Итак, если это так,
constexpr
то не могли бы вы попробоватьstd::enable_if
сget()
функцией? Кроме того, как вашаTypeIndex
обработкаstd::tuple<int, int>
? (Редактировать: я сказал function в предыдущем комментарии, я имел в виду struct .)4. Для std::tuple<int, int> он вернул бы первый int в списке и не смог бы получить второй, что нормально в моей ситуации, потому что предполагается, что у кортежа есть уникальный список.
5. Тогда не можете ли вы использовать
std::enable_if
для превращенияtemplate <typename T> Tamp; get()
в функцию, которая не компилируется, если вы вызываете с недопустимым типом? (См.: en.cppreference.com/w/cpp/types/enable_if )
Ответ №1:
Вместо рукописных TypeIndex<T, Ts...>::value
вы можете использовать typeid(T)::hash_code()
и хранить данные в std::unordered_map<size_t, boost::any>
.
std::tuple
не сохраняет информацию о базовых типах. Эта информация закодирована в типе кортежа. Итак, если ваш get
метод не может знать тип кортежа, то он не может получить смещение в нем, где хранится значение. Итак, вам нужно вернуться к динамическим методам, и наличие map является самым простым из них.
Комментарии:
1. Ваше предложение превращает конструкцию времени компиляции в конструкцию времени выполнения, вероятно, не то, что хотел OP? Хотя я не знаю, для чего предназначался OP.
2. OP хочет контейнер с полиморфизмом. В C есть статический и динамический полиморфизм. OP попросил включить контейнер в обычный класс, а не шаблон. Поэтому мы должны использовать динамический полиморфизм.
3. Смотрите мой ответ, похоже, это делается во время компиляции.
4. В вашем ответе контейнер по-прежнему является шаблоном и имеет другой тип в зависимости от сохраненных значений. Таким образом, тип кортежа не стирается.
5. Это правильный способ сделать это — я неправильно прочитал сообщение OP . 🙂
Ответ №2:
Несколько более эффективное решение, чем предложенные до сих пор, заключается в использовании std::tuple
в качестве фактического базового хранилища, что позволяет избежать использования any
или unordered_map
Если мы используем классический шаблон удаления типа, нам нужно только одно динамическое выделение (плюс все, что требуется для копирования реальных объектов), или ноль, если вы реализуете оптимизацию небольшого буфера.
Мы начнем с определения базового интерфейса для доступа к элементу по типу.
struct base
{
virtual ~base() {}
virtual void * get( std::type_info const amp; ) = 0;
};
Мы используем void*
вместо any
для возврата ссылки на объект, таким образом избегая копирования и, возможно, выделения памяти.
Фактический класс хранилища является производным от base
и шаблонизирован на основе аргументов, которые он может содержать:
template<class ... Ts>
struct impl : base
{
template<class ... Us>
impl(Us amp;amp; ... us) : data_(std::forward<Us>(us) ... )
{
//Maybe check for duplicated types and throw.
}
virtual void * get( std::type_info const amp; ti )
{
return get_helper( ti, std::index_sequence_for<Ts...>() );
}
template<std::size_t ... Indices>
void* get_helper( std::type_info const amp; ti, std::index_sequence<Indices...> )
{
//If you know that only one element of a certain type is available, you can refactor this to avoid comparing all the type_infos
const bool valid[] = { (ti == typeid(Ts)) ... };
const std::size_t c = std::count( std::begin(valid), std::end(valid), true );
if ( c != 1 )
{
throw std::runtime_error(""); // something here
}
// Pack the addresses of all the elements in an array
void * result[] = { static_cast<void*>(amp; std::get<Indices>(data_) ) ... };
// Get the index of the element we want
const int which = std::find( std::begin(valid), std::end(valid), true ) - std::begin(valid);
return result[which];
}
std::tuple<Ts ... > data_;
};
Теперь нам остается только обернуть это в типобезопасную оболочку:
class any_tuple
{
public:
any_tuple() = default; // allow empty state
template<class ... Us>
any_tuple(Us amp;amp; ... us) :
m_( new impl< std::remove_reference_t< std::remove_cv_t<Us> > ... >( std::forward<Us>(us) ... ) )
{}
template<class T>
Tamp; get()
{
if ( !m_ )
{
throw std::runtime_error(""); // something
}
return *reinterpret_cast<T*>( m_->get( typeid(T) ) );
}
template<class T>
const Tamp; get() const
{
return const_cast<any_tupleamp;>(*this).get<T>();
}
bool valid() const { return bool(m_); }
private:
std::unique_ptr< base > m_; //Possibly use small buffer optimization
};
Проверьте это в реальном режиме.
Это может быть расширено многими способами, например, вы можете добавить конструктор, который принимает фактический кортеж, вы можете получить доступ по индексу и упаковать значение в std::any
и т.д.
Ответ №3:
#include <iostream>
struct tuple_base {
virtual ~tuple_base() {}
};
template <typename T>
struct leaf : virtual tuple_base {
leaf(T const amp; t) : value(t) {}
virtual ~leaf() {}
T value;
};
template <typename ... T>
struct tuple : public leaf<T> ... {
template <typename ... U>
tuple(U amp;amp; ... u) : leaf<T>{static_cast<Uamp;amp;>(u)} ... {}
};
struct container {
tuple_base* erased_value;
template <typename T>
T amp; get() {
return dynamic_cast<leaf<T>*>(erased_value)->value;
}
};
int main() {
container c{new tuple<int, float, char>{1, 1.23f, 'c'}};
std::cout << c.get<float>() << std::endl;
}
Ключевым моментом является то, что вы должны знать больше информации о структуре типа кортежа. Невозможно извлечь информацию из реализации произвольного кортежа с удаленным типом, используя только один тип, который он содержит. Это скорее доказательство концепции, и вам, вероятно, было бы лучше использовать что-то другое, хотя это решение того, что вы просили.
Комментарии:
1. Я не думаю, что вам нужно
virtual
это реализовывать.2. используйте
= default
для деструктора. Это имеет некоторые преимущества, одно из которых заключается в том, что делает тип тривиально разрушаемым, если применимо.3. Потому что я написал это за 3 минуты. Как я уже сказал, это доказательство концепции … если кто-то действительно хочет использовать это для чего-то, очевидно, что это нужно будет очистить
4. Я полагаю, что я уберу это для всех!
5. @pat тогда, пожалуйста, сделайте, потому что в остальном это отличное решение.
Ответ №4:
Если вы согласны с использованием boost::any
, вы могли бы использовать vector
или unordered_map
из них. Вот версия, реализованная с unordered_map
:
class Container
{
public:
template<typename... Ts>
Container(std::tuple<Ts...>amp;amp; t)
{
tuple_assign(std::move(t), data, std::index_sequence_for<Ts...>{});
}
template<typename T>
T get()
{
auto it = data.find(typeid(T));
if(it == data.cend()) {
throw boost::bad_any_cast{};
} else {
return boost::any_cast<T>(it->second);
}
}
private:
std::unordered_map<std::type_index, boost::any> data;
};
И тогда вы могли бы написать почти так же, как в вашем запросе. Я изменил конструктор, чтобы он принимал кортеж, чтобы избежать множества sfinae-кода для предотвращения переопределения конструкторов копирования / перемещения, но вы можете сделать это, если пожелаете.
Container c(std::make_tuple(1, 1.5, A{42}));
try {
std::cout << "int: " << c.get<int>() << 'n';
std::cout << "double: " << c.get<double>() << 'n';
std::cout << "A: " << c.get<A>().val << 'n';
c.get<Aamp;>().val = 0;
std::cout << "A: " << c.get<A>().val << 'n';
std::cout << "B: " << c.get<B>().val << 'n'; // error
} catch (boost::bad_any_cast constamp; ex) {
std::cout << "exception: " << ex.what() << 'n';
}
Вы также могли бы указать вашему устройству Container
зафиксировать std::terminate()
вместо создания исключения.
Комментарии:
1. Почему за меня проголосовали с понижением? Это соответствует интерфейсу в соответствии с запросом.
2. Я не уверен, что моей кодовой базе прямо сейчас требуется кортеж, но этот метод хорош для возврата, если нет возможного решения.