在 C++ 中,如何创建包含可变模板对象的异构向量?

In C++, how can I create a heterogenous vector containing variafic templated objects?

嗨 Whosebug 社区!

我在工作中使用了可变参数模板、继承和抽象工厂模式,现在正在努力使它们协同工作。看来我已经达到了我目前对这些主题的了解,所以如果你能给我一个提示或代码示例,我将非常感激!提前致谢 ;)

这是上下文(我很抱歉!有几行代码...):

我有一个 Base class

template<typename... Params>
class P
{
    public:
        virtual void compute(Params&... ps) = 0;
        // other things ...

};

衍生 classes

template<typename... Params>
class AP : public P<Params...>
{
    public:
        void compute(Params&... ps) override { _compute(std::forward<Params&>(ps)...); }
    private:
        void _compute(std::string& str) {std::cout << "AP::compute str " << str << std::endl;}
};
using A = AP<std::string>;

template<typename... Params>
class BP : public P<Params...>
{
    public:
        void compute(Params&... ps) override { _compute(std::forward<Params&>(ps)...); }
    private:
        void _compute(int& i) {std::cout << "BP::compute i " << i << std::endl;}
};
using B = BP<int>;

到这里,没问题! 如果我做一个小 main(),这没有任何问题:

int main()
{
    std::unique_ptr<P<int>> p1 = std::make_unique<B>();
    int i = 15;
    p1->compute(i);

    std::unique_ptr<P<std::string>> p2 = std::make_unique<A>();
    std::string str = "abc";
    p2->compute(str);
}

但是,我们可以添加更多:工厂的 Base classes。 (这些将与我的 class P 以外的其他 classes 一起使用...如果您想知道为什么 :) )

template<typename Base>
class Creator
{
    public:
        virtual std::unique_ptr<Base> create() = 0;
};

template<class Key, class T>
class Factory
{
    public:
        void store(Key key, std::unique_ptr<Creator<T>>&& creator)
        {
            _crs[key] = std::move(creator);
        }

        std::unique_ptr<T> create(Key key)
        {
            return _crs[key]->create();
        }

    private:
        std::map<Key, std::unique_ptr<Creator<T>>> _crs;
};

及其实现能够构建 P 相关对象:

template<typename Derived, typename... Params>
class PCreator : public Creator<P<Params...>>
{
    public:
        std::unique_ptr<P<Params...>> create() override
        {
            return std::make_unique<Derived>();
        }
};

template<typename... Params>
class PFactory : public Factory<std::string, P<Params...>>
{
    public:
        PFactory()
        {
            this->store("int", std::make_unique<PCreator<BP<int>>>);
            this->store("string", std::make_unique<PCreator<AP<std::string>>>);
        }
        // create() and store() methods inherited
};

如果我实例化 PFactory,编译器显然不能完成它的工作,因为它需要 PFactory 的模板参数,这会将它们转发给Factory<std::string, P<Params...>>.

但是,我的工厂只能创建一个 "type" of P 对象,可以使用这些 Params 的对象。 这是我一个人走的路(遗憾的是我的同事都没有能力帮助我...)

我的目标是能够写出这样的东西:

class Thing
{
    const std::array<std::string, 2> a = {"one", "two"};
    public:
        Thing()
        {
            PFactory f;
            for(const auto& e : a)
                _ps[e] = std::move(f.create(e));
        }

        void compute()
        {
            int i = 100;
            std::string str = "qwerty";
            // additional computations...
            _ps["one"]->compute(i);
            // additional computations...
            _ps["two"]->compute(str);
        }

    private:
        std::map<std::string, std::unique_ptr<P>> _ps;
};

这是我尝试在 CompilerExplorer 上工作和返工的 PoC,上面的来源是从哪里来的。

任何帮助将不胜感激!


[编辑] 是的,我妄想我可以欺骗编译器来创建带有运行时信息的各种方法签名。

解决方案总结:

(@walnut: thanks!) let compute take std::any or something like that

我不太了解 std::any,但在 rtfm-ing CppReference 之后,它可以完成这项工作,接受这个事实,我需要将参数转换回我需要的参数它在我的 Derived classes 中(并处理异常)。 可悲的是,在实际项目中,compute() 可以采用多个参数(我使用可变参数模板的原因......我不想关心每个 [=21= 中参数的数量或类型] 方法在每个 Derived class) 中,所以它会迫使我创建 compute(const std::any&)compute(const std::any&, const std::any&),等等

(@MaxLanghof: thanks!) One (ugly) solution is to provide all possible compute overloads as virtual functions by hand.

是的,你说得对,我也觉得很奇怪(我不会说到“丑陋”,但我还没有更漂亮的解决方案,所以...),但它正在运行。 这里的缺点是我无法将 class P(和相关的 classes)存储在它自己的库中开始分离关注点(«MainProgram» 玩 Ps 派生自 lib::P)。

(@MaxLanghof: thanks!) to do the entire _ps mapping at compile-time.

我还没有足够的 C++ 经验和知识来实现​​这样的事情。我需要努力解决这个问题,如果有人有具体的 link(我的意思是:不是 Google 上的第一个 link ;))或示例,我很乐意学习。

感谢您到目前为止的回答!


[编辑] 嗨!抱歉耽搁了,我刚刚回到这个项目,非常感谢你的想法和经验!这意义重大!

我在@Caleth 和@KonstantinStupnik 的工作中做了一些工作(非常感谢您的示例:它们帮助我理解了我在做什么!)并通过我的测试达到了这一点案例:https://gcc.godbolt.org/z/AJ8Lsm 我遇到了 std::bad_any_cast 异常,但不明白为什么...

我怀疑引用传递或我使用 lambda 在 std::any 中保存 compute 方法的方式有问题,但不能确定。 我试图扩展 AnyCallable<void>::operator() 中接收到的类型,以发现与 P 的构造函数中存储函数的区别,但对我来说似乎是一样的。

我试图将 &AP::compute 传递给 P 的构造函数,但是编译器无法再推断出参数类型...

感谢大家的宝贵时间、帮助和建议!

您可以对参数进行类型擦除,只要您可以在调用站点指定它们即可。

至少:

#include <functional>
#include <any>
#include <map>
#include <iostream>

template<typename Ret>
struct AnyCallable
{
    AnyCallable() {}
    template<typename F>
    AnyCallable(F&& fun) : AnyCallable(std::function(fun)) {}
    template<typename ... Args>
    AnyCallable(std::function<Ret(Args...)> fun) : m_any(fun) {}
    template<typename ... Args>
    Ret operator()(Args&& ... args) 
    { 
        return std::invoke(std::any_cast<std::function<Ret(Args...)>>(m_any), std::forward<Args>(args)...); 
    }
    template<typename ... Args>
    Ret compute(Args ... args) 
    { 
        return operator()(std::forward<Args>(args)...); 
    }
    std::any m_any;
};

template<>
struct AnyCallable<void>
{
    AnyCallable() {}
    template<typename F>
    AnyCallable(F&& fun) : AnyCallable(std::function(fun)) {}
    template<typename ... Args>
    AnyCallable(std::function<void(Args...)> fun) : m_any(fun) {}
    template<typename ... Args>
    void operator()(Args&& ... args) 
    { 
        std::invoke(std::any_cast<std::function<void(Args...)>>(m_any), std::forward<Args>(args)...); 
    }
    template<typename ... Args>
    void compute(Args ... args) 
    { 
        operator()(std::forward<Args>(args)...); 
    }
    std::any m_any;
};

using P = AnyCallable<void>;

void A(std::string& str) {std::cout << "AP::compute i " << str << std::endl;}
void B(int i) {std::cout << "BP::compute i " << i << std::endl;}

class Thing
{
    public:
        Thing(){}

        void compute()
        {
            int i = 100;
            std::string str = "qwerty";
            // additional computations...
            ps["one"].compute<int>(i);
            // additional computations...
            ps["two"].compute<std::string&>(str);
        }

    private:
        std::map<std::string, P> ps = { { "one", B }, { "two", A } };
};

Or with all the Factory