将类模板的实例存储在另一个类模板中

jjc*_*mar 5 c++ templates tuples

假设我有一个如下所示的模板

template <typename T>
struct A {};
Run Code Online (Sandbox Code Playgroud)

在另一个类中存储该模板的不同实例化向量的最佳方法是什么?

我考虑过类似的事情,但是当然,它迫使我声明所有我不想支持的类型,在我的特殊情况下,这可能会迅速升级。

template <typename ...Args>
struct Holder
{

std::tuple<std::vector<Args>...> tup;

template <typename T>
void push_back(T &&t); // Method to insert in the appropiate vector, considering only one match
}
Run Code Online (Sandbox Code Playgroud)

鉴于我有 Push_back 方法,我用它来将元素插入元组中的适当向量中,并且除了使用 Push_back 方法插入的类型之外我不需要任何其他类型,是否有更好的方法来定义持有者?

编辑:理想情况下,我希望能够在元组的不同元素上调用函数模板。

通用调用

template <typename T>
void foo(T t)
Run Code Online (Sandbox Code Playgroud)

对 A 的实际实例化的专门调用

template <typename T>
void foo(A<T> t)
Run Code Online (Sandbox Code Playgroud)

Hum*_*ler 0

听起来您正在寻找的模式称为“类型擦除” ——它允许以某种同类方式收集和访问异构类型的集合。

在评论中,您指出了两个重要组成部分:

  1. 这都是价值语义(拥有),并且
  2. 您正在寻找非成员draw(A)函数调用。

为了执行类型擦除,我们需要有一种方法将对象存储为某种同类类型,并且您将需要某种方法来访问重要的功能。请注意,类型擦除通常是一个单向过程(例如,一旦擦除,很难在不轮询的情况下重新提取原始类型。想想如何std::function操作)。

您可以通过以下几种方法来实现此目的:

选项 1:使用std::any(C++17 及更高版本)

有点现代的方法是结合使用 C++17 的std::any类型和一种机制来提取原始类型并生成所需的调用。最简单的机制是使用函数指针,它将指向静态函数模板以保留原始类型。考虑以下(已记录的)示例:

template <typename T>
class A { /* ... */ };

auto draw(const A<int>&) -> void;
auto draw(const A<long>&) -> void;
// ...

// Named 'drawable' since you indicated 'draw(A)' calls
class Drawable
{
private:

    // A handler function to perform the operation.
    // Each pointer will "know" the original T type
    using handler_fn_t = auto (*)(const std::any&) -> void;

    std::any m_storage;
    handler_fn_t m_handler;
   
    // The handler function. This knows the original type bound
    // to the std::any for an easy cast back, and then calls the
    // desired function.
    template <typename T>
    static auto type_stub(const std::any& storage) -> void {
        const auto* p = std::any_cast<T>(&storage);
        
        // Call the free-function named 'draw'
        draw(*p);
    }

public:

    // A constructor that just constructs the object
    // This uses SFINAE to constrain to functions where 'draw(...)' is 
    // well-formed. If you are using C++20, this can be a "requires" clause.
    template <typename T, typename = decltype(draw(std::declval<const T&>()))>
    Drawable(const T& t)
        : m_storage{t}, m_handler{type_stub<T>}
    {
    }

    Drawable(const Drawable&) = default;    

    // The draw function. This just calls the underlying handler
    auto draw() const -> void {
        // Call the function stub
        (*m_handler)(m_storage);
    }
};
Run Code Online (Sandbox Code Playgroud)

Live Example

上面假设仅使用 1 个单个函数,具有一个特定的可变性 ( const)。如果需要更多功能,可以修改函数指针技术以提供“有效负载”参数和“操作”枚举来区分该值。any“有效负载”为给定操作提供正确的 CV 限定,这可以节省const_cast

class Drawable
{
private:
    enum class operation {
        draw,
        get_something,
        set_something,
    };

    // Modified to take an "operation" and "void*" param
    using handler_fn_t = auto (*)(operation, void*) -> void;

    struct draw_payload {
        const std::any& storage;
    };
    struct get_payload {
        const std::any& storage;
        int some_value_to_return;
    };
    struct set_payload {
        std::any& storage;
        int some_value_to_set;
    };

    std::any m_storage;
    handler_fn_t m_handler;
   
    // Modified to take the operation and the payload
    template <typename T>
    static auto type_stub(operation op, 
                          void* payload) -> void 
    {
        const auto* p = std::any_cast<T>(&storage);
        switch (op) {
            case operation::draw: { 
                auto* payload_type = static_cast<draw_payload*>(payload); 
                draw(*std::any_cast<T>(&payload_type->storage));
                break;
            }
            case operation::get_something: { 
                auto* payload_type = static_cast<get_payload*>(payload);
                out_payload->some_value_to_return = p->get_value();
                break;
            }
            case operation::set_something: { 
                auto* payload_type = static_cast<set_payload*>(payload);
                p->set_value(in_payload->some_value_to_set);
                break;
            }
        }
    }

public:
    
    ...

    // The draw function. This just calls the underlying handler
    auto draw() const -> void {
        auto payload = draw_payload{m_storage};
        // Call the function stub
        (*m_handler)(operation::draw, &payload);
    }
    auto get() const -> int {
        auto payload = get_payload{m_storage};
        (*m_handler)(operation::get, &payload);
        return payload.some_value_to_return;
    }
    auto set(int value) -> void {
        auto payload = set_payload{m_storage, value};

        (*m_handler)(operation::set, &payload);
    }
};
Run Code Online (Sandbox Code Playgroud)

Live Example

总的来说,这是一项非常强大的技术。

选项 2:通过模板创建层次结构

另一种选择编写起来更自然,但利用了虚拟层次结构和堆内存unique_ptr

这个想法只是在类中获取一个T类型,并生成一个所需的接口,以及一个将该类型包装T成满足该接口的实现。

使用上面的第二个例子,也可以写成:

class Drawable
{
private:

    // The interface we want each 'T' to subscribe to
    struct Interface {
        virtual ~Interface() = default;
        virtual auto draw() const -> void = 0;
        virtual auto get() const -> int = 0;
        virtual auto set(int) -> void = 0;
    };

    // An implementation that satisfies the interface, in terms of T
    template <typename T>
    struct Concrete : Interface {
        explicit Concrete(const T& v) : value{v}{}

        T value;
        auto draw() const -> void override { return ::draw(value); }
        auto get() const -> int override { return value.get_value(); }
        auto set(int v) -> void override { value.set_value(v); }
    };
    
    // Something to hold onto the T type.
    // This doesn't necessarily have to be a unique_ptr; just something
    // that holds onto the interface in some way.
    std::unique_ptr<Interface> m_interface;

public:
  
    // Similar constructor to before; we create a concrete pointer, and let
    // it erase into the Interface
    template <typename T, typename = decltype(draw(std::declval<const T&>()))>
    Drawable(const T& t)
        : m_interface{std::make_unique<Concrete<T>>(t)}
    {
    }

    Drawable(Drawable&&) = default;

    // Just call the basic underlying types
    auto draw() const -> void {
        m_interface->draw();
    }
    auto get() const -> int {
        return m_interface->get();
    }
    auto set(int v) -> void {
        m_interface->set(v);
    }
};
Run Code Online (Sandbox Code Playgroud)

Live Example

正如您所看到的,这种方法写起来更“自然”——但通常也需要某种形式的分配内存,这可能会导致碎片,并且可能是不可取的。这也可以通过固定大小的缓冲区和指针来完成,但这也会导致一些冗余。


这些只是解决所描述问题的一些可能方法