【问题标题】:metaprogramming with variadic templates in C++在 C++ 中使用可变参数模板进行元编程
【发布时间】:2015-02-15 08:29:38
【问题描述】:

我正在开发一个简单的游戏引擎,它为游戏对象提供了一个基类,可以使用特定游戏的子类进行扩展。我需要编写一个函数,它可以获取一个文件,从中解析对象名称,并在游戏中实例化相应的对象;提供一种在文件中存储关卡数据的机制。我曾希望使用元编程来创建一个函数,允许调用者传入可变数量的数据类型,并生成一个函数来搜索与文件中这些类型对应的名称。它的使用看起来像这样(使用模板):

fileParseFunction<type1, type2 type3>("filename");

将生成一个等效于:

fileParseFunction(string filename)
{
    //code that opens file called "filename" and handles tokenizing/parsing

    if(token == "type1")
    {
        gameWorld.add(new type1());
    }
    elseif(token == "type2")
    {
        gameWorld.add(new type2());
    }
    elseif(token == "type3")
    {
        gameWorld.add(new type3());
    }

    //other code to finish loading the level 
}

使用参数“文件名”调用。这应该适用于可变数量的类型(示例中为 3)。我编写了一些代码来测试涉及生成类似函数的概念。它使用模板将类型名称符号转换为字符串(在我最终希望编写的函数中进行比较时需要使用它),还使用可变参数模板生成一个函数,该函数打印作为模板参数传入的所有类型的名称。这里是:

#define TypeNameTemplate(a) template<> inline const char* typeName<a>(void) { return #a; }

template <typename T>
inline const char* typeName(void) { return "unknown"; }

TypeNameTemplate(int);
TypeNameTemplate(std::string);
TypeNameTemplate(double);
TypeNameTemplate(bool);
TypeNameTemplate(float);

/*template <>
inline const char* typeName<int>(void) { return "int"; }*/

template <typename T> inline void printtypes()
{
    std::cout << typeName<T>();
}

template <typename T, typename... Args> void printtypes()
{
    std::cout << typeName<T>() << std::endl;
    printtypes<Args...>();
}

using namespace std;

int main()
{
    //string a = typeName<int>();
    //print();
    printtypes<int, double, string, bool, float>();
    return 0;
}

printtypes() 应该生成一个等效于:

void printtypes()
{
    std::cout << typeName<int>();
    std::cout << typeName<std:string>();
    std::cout << typeName<double>();
    std::cout << typeName<bool>();
    std::cout << typeName<float>();
}

但是,在编译过程中我得到了这个错误:

E:\C++ projects\templateTest\main.cpp:26:5: 注意:候选人是: E:\C++ 项目\templateTest\main.cpp:18:35: 注意:void printtypes() [with T = float] E:\C++ 项目\templateTest\main.cpp:23:46: 注意:void printtypes() [with T = float; Args = {}]

似乎在递归到达可变参数包的末尾时,编译器不知道是调用仅针对具有最后一个类型的一种类型的模板,还是调用具有最后一个类型的可变参数模板pack 加上一个空的参数包。我在 C++ 中尝试做的事情是否可行/实用,有没有办法让编译器知道它应该将单参数模板用于递归调用的基本/最终情况?

【问题讨论】:

    标签: c++ templates recursion metaprogramming game-engine


    【解决方案1】:

    一个简单的解决方案是在第二个重载中添加另一个显式参数:

    template <typename T, typename T2, typename... Args> void printtypes()
    {
        std::cout << typeName<T>() << std::endl;
        printtypes<T2,Args...>();
    }
    

    【讨论】:

    • 您的方法可能是最简单的。我因为羞耻而删除了我的;)
    【解决方案2】:

    虽然这个答案没有回答您关于可变参数模板的具体问题,但我希望它确实回答了您关于如何使您的游戏引擎可扩展的基本问题。

    您通过创建fileParseFunction() 想出的是Factory Method 模式的实现。这是使存储数据轻松转换为真实对象的主要部分。不幸的是,它违反了Open-Close Principle,因此很难达到您的最终目标,即可扩展性。

    例如,在您上面的代码中,您的工厂函数可以从数据文件中解析“type1”、“type2”和“type3”,并生成type1type2type3 的对象,但会添加更多类型这意味着编辑此函数并为您希望添加的每个新类型添加一个新的else if

    您已经确定这是一个问题,并正在尝试使用可变参数模板来解决它。不幸的是,如果您将游戏对象的数量扩展到二十、三十甚至数百种类型,可变参数模板使用起来会变得很麻烦,如果它们能够做到那么远的话。

    一个更简单的解决方案是使用Abstract Factory 模式。这实质上将创建游戏对象的责任从文件解析器的工厂函数转移到了工厂对象。这种权力转移是针对单一职能还是一个成熟的班级取决于您。你也可以模板化这个工厂来节省编码。

    您的每个工厂都必须在调用解析器之前向文件解析器注册它们的存在,扩展解析器的功能就像创建一个新工厂并将其注册到解析器一样简单。

    一个简单的例子是:

    class GameObjectAbstractFactory {
    public:
        string registeredTypes() const{
            // cycle through hash table to return the names of registered factories
        }
    
        GameObjectFactory* getFactory(string factoryName){
            // return the registered factory, or nullptr if not registered
        }
    
        void registerFactory(string factoryName, GameObjectFactory*){
            // add the factory if it doesn't exist
        }
    
        static GameObjectAbstractFactory* instance(){
            // access to Singleton instance
        }
    
    private:
        GameObjectAbstractFactory(); // enforces Singleton pattern
    
        Hash<string, GameObjectFactory*> registeredFactories;
    };
    
    
    // Interface class for generating concrete types, can be templatised, depending on implementation
    class GameObjectFactory{
    public:
        string name() = 0;
        GameObject *createObject() = 0;
    };
    

    这会改变您的解析函数,使其变为:

    fileParseFunction(string filename)
    {
        //code that opens file called "filename" and handles tokenizing/parsing
    
        GameObjectAbstractFactory *abstractFactory = GameObjectAbstractFactory::instance();
    
        GameObjectFactory *factory = abstractFactory.getFactory(token);
        if(factory != nullptr)
        {
            gameWorld.add(factory.createObject());
        }
    
        //other code to finish loading the level 
    }
    

    这将使您的fileParseFunction() 符合开闭原则,因为它仍然可以在您的引擎扩展时生成新的游戏对象,但函数本身不必为此进行修改。

    但这种模式有一个警告:所有工厂都需要在需要之前向抽象工厂注册,否则将无法创建所需的游戏对象。

    正如我在开头提到的,这个答案并没有解决您关于可变参数模板的直接问题,但我希望这有助于您的游戏引擎的可扩展性。

    【讨论】:

    • 这看起来是一个更好的解决方案,非常有趣。看起来我有一些关于设计模式的阅读要做!感谢您的建议。
    猜你喜欢
    • 2012-09-17
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2018-02-20
    • 2018-11-11
    相关资源
    最近更新 更多