【问题标题】:In C++, how can I create a heterogenous vector containing variafic templated objects?在 C++ 中,如何创建包含可变参数模板对象的异构向量?
【发布时间】:2020-01-31 12:03:30
【问题描述】:

你好 StackOverflow 社区!

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

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

我有一个 Base

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

};

派生的

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 类。 (这些将与我的班级以外的其他班级一起使用P...如果您想知道为什么:))

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&lt;std::string, P&lt;Params...&gt;&gt;

但是,我的工厂只能创建一种“类型”的 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: 谢谢!) 让 compute 采用 std::any 或类似的东西

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

(@MaxLanghof:谢谢!)一种(丑陋的)解决方案是手动提供所有可能的计算过载作为虚函数。

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

(@MaxLanghof:谢谢!)在编译时完成整个 _ps 映射。

我还没有足够的 C++ 经验和知识来完成这样的事情。我需要解决这个问题,如果有人有特定的链接(我的意思是:不是 Google 上的第一个链接;))或示例,我会很高兴学习。

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


[编辑]嗨!抱歉耽搁了,我刚刚回到这个项目,非常感谢您的想法和经验!这意味着很多!

我在 @Caleth 和 @KonstantinStupnik 的工作上做了一些工作(真的很感谢你的例子:他们帮助我理解了我在做什么!)并带着我的测试用例到达了这一点: https://gcc.godbolt.org/z/AJ8Lsm 我遇到了 std::bad_any_cast 异常,但不明白为什么......

我怀疑传递引用或我使用 lambda 将 compute 方法保存在 std::any 中的方式存在问题,但不能确定。 我尝试扩展在AnyCallable&lt;void&gt;::operator() 中接收的类型,以找到与 P 构造函数中存储函数的区别,但对我来说似乎相同。

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

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

【问题讨论】:

  • 我不确定这一切的意义何在,但基本上:_ps["one"]_ps["two"] 具有相同的静态类型。所以compute必须是接受不同类型的模板,在这种情况下它不能是virtual,或者它必须为它在基类中接受的不同类型重载。目前compute 是基类中的一个非重载函数。那是行不通的。
  • 我看到的让它以这种方式工作的唯一方法是让compute 采用std::any 或类似的东西,但是调用者仍然必须知道@中对象的派生类型987654350@ apriori 为了不提供错误的参数类型,在这种情况下使用带有基类指针的容器是没有意义的。
  • 编译器需要在编译时知道(*_ps["..."])的类型。由于该类型是在编译时确定的,因此它不能依赖于运行时值。您希望该类型具有 compute(i)compute(str) 方法。但是您不能拥有 virtual 和模板化的成员函数。一种(丑陋的)解决方案是手动提供所有可能的compute 重载作为虚函数。你对_ps["one"]-&gt;compute(str); 有什么期望?
  • 另一种可能的解决方案是在编译时进行整个_ps 映射。但是你不能(容易)使用运行时字符串值而不是例如"one"。通常,类型擦除要求 a) 整个接口是(非模板化)基类的一部分,或者 b) 调用者/用户代码知道他们需要检索什么类型。您目前两者都不满足,如果您想要一个异构容器,则必须选择其中一个选项。
  • template&lt;typename... Params&gt; class AP 并没有真正起作用,只有 AP&lt;std::string&gt; 会起作用。

标签: c++ templates inheritance variadic-templates factory


【解决方案1】:

您可以键入-擦除参数,只要您可以在调用站点指定它们。

最低限度:

#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

【讨论】:

  • 非常感谢@Caleth!到目前为止,您的示例使我了解了很多事情。我用你的工作来尝试更好地我的工作,这导致了我的帖子的第二次编辑(很遗憾)。再次感谢您的帮助!
猜你喜欢
  • 1970-01-01
  • 2020-05-22
  • 1970-01-01
  • 2021-12-11
  • 2022-01-13
  • 2020-07-18
  • 2019-10-09
  • 2016-10-05
  • 1970-01-01
相关资源
最近更新 更多