在c++11中,为包含std::元组的对象键入Erasure

Type Erasure for objects containing a std::tuple in c++11

本文关键字:元组 对象 Erasure std c++11 包含      更新时间:2023-10-16

假设我有一个泛型类Container,它包含任何类型的元组,并且有一个函数template<typename T> T& get<T>();,它返回对元组中元素的引用。我非常简单的实现如下:

template<typename... Ts>
class Container
{
    std::tuple<Ts...> contents;
    public:
    Container(const Ts&... ts) : contents(ts...) {}
    template <typename T>
    T& get()
    {
        //TypeIndex is some meta-programming struct to find index of T in Ts
        return std::get<TypeIndex<T, Ts...>::value>(contents);
    }
};

有没有什么好的类型擦除技术可以在不更改get函数签名的情况下将Container转换为常规类?就像在不知道元组完整类型列表的情况下调用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>
    A& a = container.get<A>(); //Doesn't know the tuples type list but assumes A is in there.
    C& 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> {};

您可以使用typeid(T)::hash_code()并将数据存储在std::unordered_map<size_t, boost::any>中,而不是手工编写的TypeIndex<T, Ts...>::value

std::tuple不存储有关基础类型的信息。该信息被编码在元组的类型中。因此,如果get方法不知道元组的类型,那么它就无法在存储值的位置获得偏移量。因此,您必须恢复到动态方法,而拥有映射是最简单的方法。

与目前提出的解决方案相比,更有效的解决方案是使用std::tuple作为实际的底层存储,从而避免使用anyunordered_map

如果我们使用经典的类型擦除模式,我们只需要一个动态分配(加上复制实际对象所需的任何东西(,或者如果实现小缓冲区优化,则需要零。

我们首先定义一个基本接口来按类型访问元素。

struct base
{
    virtual ~base() {}
    virtual void * get( std::type_info const & ) = 0;
};

我们使用void*而不是any来返回对对象的引用,从而避免了复制和可能的内存分配。

实际的存储类是从base派生的,并以它可以包含的参数为模板:

template<class ... Ts>
struct impl : base
{
    template<class ... Us>
    impl(Us && ... us) : data_(std::forward<Us>(us) ... ) 
    {
        //Maybe check for duplicated types and throw.
    }
    virtual void * get( std::type_info const & ti ) 
    {
        return get_helper( ti, std::index_sequence_for<Ts...>() );
    }
    template<std::size_t ... Indices>
    void* get_helper( std::type_info const & 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*>(& 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 && ... us) :
            m_( new impl< std::remove_reference_t< std::remove_cv_t<Us> > ... >( std::forward<Us>(us) ... ) )
       {}
     template<class T>
     T& get()
     {
        if ( !m_ )
        {
            throw std::runtime_error(""); // something
        }
        return *reinterpret_cast<T*>( m_->get( typeid(T) ) );
     }
     template<class T>
     const T& get() const
     {
         return const_cast<any_tuple&>(*this).get<T>();
     }
     bool valid() const { return bool(m_); }
 private:
     std::unique_ptr< base > m_; //Possibly use small buffer optimization
 };

实时查看。

这可以通过多种方式进一步扩展,例如,您可以添加一个接受实际元组的构造函数,您可以通过索引访问并将值打包到std::any中,等等。

#include <iostream>
struct tuple_base {
    virtual ~tuple_base() {}
};
template <typename T>
struct leaf : virtual tuple_base {
    leaf(T const & t) : value(t) {}
    virtual ~leaf() {}
    T value;
};
template <typename ... T>
struct tuple : public leaf<T> ... {
    template <typename ... U>
    tuple(U && ... u) : leaf<T>{static_cast<U&&>(u)} ... {}
};
struct container {  
    tuple_base* erased_value;
    template <typename T>
    T & 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;
}

关键是您必须了解有关元组类型结构的更多信息。仅使用其包含的单个类型是不可能从类型擦除的任意元组实现中提取信息的。这更多的是一种概念验证,你可能会更好地使用其他东西,尽管这是你所要求的解决方案。

如果您可以使用boost::any,则可以使用其中的vectorunordered_map。以下是用unordered_map:实现的版本

class Container
{
public:
    template<typename... Ts>
    Container(std::tuple<Ts...>&& 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<A&>().val = 0;
    std::cout << "A: " << c.get<A>().val << 'n';
    std::cout << "B: " << c.get<B>().val << 'n'; // error
} catch (boost::bad_any_cast const& ex) {
    std::cout << "exception: " << ex.what() << 'n';
}

现场演示

您还可以指示Container提交std::terminate(),而不是抛出异常。