授予另一个 class 访问特定方法的权限

Give another class access to specific methods

我在暑假期间将游戏引擎作为一个项目进行研究。每个可编写脚本的组件都应该能够访问它们所在场景中的某些方法。为了实现这一点,我将调用相应方法的场景中的 lambda 表达式传递给可编写脚本的组件,在那里它们被隐式转换为 std::function 类型。

Scene.h:

class Scene
{
private:
    unsigned int _currentId;
    std::vector<System*> _systems;

    //SCRIPTABLE NEEDS THE BELOW METHODS THESE EXCLUSIVELY:

    bool exists(unsigned id);
    void destroy(unsigned int);
    void addComponent(Component*, unsigned int);
    template<typename T> T& getComponent(unsigned int);
    template<typename T> bool hasComponent(unsigned int);
    template<typename T> void removeComponent(unsigned int);

protected:
    unsigned int instantiate(std::vector<Component*>);

public:
    Scene(ChangeSceneCallback);
    ~Scene();
    void initiate();
    void update(long dt);
};

template<typename T>
inline T & Scene::getComponent(unsigned int id)
{
    for (System* system : _systems) {
        if (system->corresponds(T)) {
            return static_cast<T*>(system->getComponent(entityId));
        }
    }
}

template<typename T>
inline bool Scene::hasComponent(unsigned int id)
{
    for (System* system : _systems) {
        if (system->corresponds(T)) {
            return system->contains(id);
        }
    }
}

template<typename T>
inline void Scene::removeComponent(unsigned int id)
{
    for (System* system : _systems) {
        if (system->corresponds(T)) {
            return system->destroy(id);
        }
    }
}

回调方法适用于我需要访问的非模板函数,但不适用于模板函数,所以这是不可能的。

可编写脚本:

typedef std::function<void(int)>                                    ChangeSceneCallback;
typedef std::function<int(std::vector<Component*>)>                 InstantiateCallback;
typedef std::function<void(int)>                                    DestroyCallback;
typedef std::function<bool(int)>                                    ExistCallback;
typedef std::function<void(Component*, unsigned int)>               AddComponentCallback;


class Scriptable: public Component
{
protected:
    ChangeSceneCallback changeScene;
    InstantiateCallback instantiate;
    DestroyCallback destroy;
    ExistCallback exists;
public:
    ~Scriptable();
    Scriptable();
    void assignCallbacks(ChangeSceneCallback, InstantiateCallback etc ...);

    virtual void init() = 0;
    virtual void update() = 0;
}; 

Scriptable 无法访问场景中的 public 方法,因为这会给用户/开发人员访问它们的权限(Scriptable 是游戏行为的基础 class)。这就是为什么我需要想出一些东西来提供对场景的可脚本化有限访问。

有什么想法吗?

您不能删除类型 "template callback"。您必须在模板或类型擦除之间做出选择。让我解释一下。

这就是 "template callback" 的样子。这实际上是一个通用的 lambda:

auto print_callback = [](auto var) {
    std::cout << var << std::endl;
}

print_callback(4) ;      // prints "4"
print_callback(4.5);     // prints "4.5"
print_callback("hello"); // prints "hello"

看起来不错,但请注意,您不能使用 std::function 执行此操作,因为您必须预定义签名。

std::function<void(int)> func_print_callback = print_callback;

func_print_callback(5); // Yay! Prints "5"
func_print_callback("hello"); // error

事实是,您可能认为限制只是因为 std::function 需要特定的签名才能使用,但限制远不止于此。

事实是,没有 模板函数。它们不存在。 另一方面,函数模板确实存在。为什么我如此强调我的话的顺序是因为这个东西的名字说明了一切:它不是一个 function,它是一个 template 用于制作函数。

这是一个简单的例子:

template<typename T>
void foo(T t) {
    std::cout << t << std::endl;
}

此函数未编译。因为它不是函数。在空洞 T 被填满之前,函数 foo 将不存在。

How do you fill the hole named T supposed to be a type?

填写课程类型!

foo(5.4); // the hole T is `double`

当编译器看到这个时,它知道你需要一个名为 foo 的函数,它接受一个双精度数作为参数。没有名为 foo 的函数接受 double。但是我们给了编译器一个创建模板的工具:模板!

所以编译器会生成这个函数:

void foo_double(double t) {
    std::cout << t std::endl;
}

这里的词是这样的:生成。编译器需要创建函数才能存在。编译器为您生成代码。

函数生成编译后,T不存在了。模板参数是一个编译时实体,只有编译器知道它们。


现在,我将向您解释为什么没有模板回调这样的东西。

类型擦除容器如std::function是用指向函数的指针实现的。我将使用类型别名来简化语法。它是这样工作的:

// A function
void foo(int) {}

// The type of the pointer to function
using func_ptr = void(*)(int);

// A pointer to foo
func_ptr ptr = &foo;

指向函数 foo 的指针有一个指向 foo 在内存中的位置的值。

现在想象一下我们有办法拥有模板函数指针。我们将不得不指向一个尚不存在的函数。它没有内存位置,因此没有意义。通过指针,当作为函数调用时,您必须生成函数代码。

由于指向函数的指针可以指向任何函数,甚至是编译器尚不知道的函数,因此您必须以某种方式生成函数代码并对其进行编译。但是指针的值,也就是我们的指针指向的函数,是在运行时定义的!所以你必须在运行时编译代码,对于你还不知道的代码,当编译器不再存在时,从一个不存在的值开始。如您所见,指向模板函数、模板 std::function 或虚拟模板函数的指针不能存在。


既然你已经理解了问题,让我提出一个解决方案:放弃回调的使用。您应该直接调用这些函数。

您使用回调似乎只是为了能够调用私有成员函数。这是错误的做法,即使它有效。你需要的是friend,C++允许你访问私有成员的特性

class Scene {
    friend Component;

    // ...
};

class Component {
protected:

    // Let `scene` be a reference to your scene

    void addComponent(Component* c, unsigned int id) {
        scene.addComponent(c, id);
    }

    template<typename T>
    T& getComponent(unsigned int id) {
        return scene.getComponent<T>(id);
    }

    template<typename T>
    bool hasComponent(unsigned int id) {
        return scene.hasComponent(id);
    }

    template<typename T>
    void removeComponent(unsigned int id) {
        removeComponent(id);
    }

    // ...
};

由于 Component class 是 Scene 的唯一朋友,因此只有它可以调用私有成员函数。由于 Component 中所有新定义的函数都受到保护,因此只有从 Component 扩展的 class 可以调用这些函数。它们是这样调用的:

class Scriptable : public Component {
    void foo() {
        hasComponent<Bar>(87); // works, call function defined in `Component`
    }
};