如何在编译时检查模板方法是否被调用?

6
我正在编写一个 实体组件系统游戏引擎。作为其中的一部分,我已经编写了一个 Manager 类,该类将注册各种 IBase 实现,并稍后允许我实例化这些实现。下面是我希望使用它的示例。
class Manager{
    public:
        template<class T>
        void registerDerived()
        { /*Register a Derived with the Manager*/ };

        template<class T>
        T createDerived()
        {   /*if T is not registered, throw an error*/
            return T();};
};

struct IBase{
};

struct Derived1 : public IBase{
};

struct Derived2 : public IBase{
};

如评论中所述,我在template<class T>Manager::createDerived()中的代码检查了是否使用template<class T>Manager::registerDerived()注册了Base的特定实现,如果没有注册,则会抛出错误。这个检查很简单,为了保持简单性,代码示例中省略了它。
我的问题是:是否可能将此检查移动到编译时,而不是等待运行时?似乎在运行时应该有足够的信息来做出这个决定。
到目前为止,我已经探索/阅读了与SFINAE相关的内容,这似乎是要采取的方法,但我无法弄清楚如何使这些惯用语在这种特定情况下起作用。此链接提供了基本SFINAE惯用语的良好概述,此SO问题提供了一些很好的代码片段,最后此博客文章似乎几乎涵盖了我的确切情况。
以下是一个完整示例,其中包含我尝试实现这些链接中找到的信息:
#include <iostream>

class Manager{
    public:
        template<class T>
        void registerDerived()
        { /*Register a Derived with the Manager*/ }

        template<class T>
        T createDerived()
        {   /*if T is not registered, throw an error*/
            return T();}
};

struct IBase{
};

struct Derived1 : public IBase{
};

struct Derived2 : public IBase{
};


template<typename T>
struct hasRegisterDerivedMethod{
    template <class, class> class checker;

    template <typename C>
    static std::true_type test(checker<C, decltype(&Manager::template registerDerived<T>)> *);

    template <typename C>
    static std::false_type test(...);

    typedef decltype(test<T>(nullptr)) type;
    static const bool value = std::is_same<std::true_type, decltype(test<T>(nullptr))>::value;
};


int main(){
    Manager myManager;
    myManager.registerDerived<Derived1>();
    // whoops, forgot to register Derived2!
    Derived1 d1 = myManager.createDerived<Derived1>(); // compiles fine, runs fine. This is expected.
    Derived2 d2 = myManager.createDerived<Derived2>(); // compiles fine, fails at runtime (due to check in createDerived)

    std::cout << std::boolalpha;

    // expect true, actual true
    std::cout << "Derived1 check = " << hasRegisterDerivedMethod<Derived1>::value << std::endl;
    // expect false, actual true
    std::cout << "Derived2 check = " << hasRegisterDerivedMethod<Derived2>::value << std::endl;

    return 0;
}

**

简述

如何修改上述代码以产生编译时错误(可能使用 static_assert),而不是等到运行时才检测到错误?

**


很抱歉,这在编译时不可能实现。您不能在编译时将“状态”附加到非constexpr对象并同时在编译时读取该状态。 - WhiZTiM
Manager 是一个单例模式吗? - Walter
create()替换为后跟create()register()有什么问题(但在一个函数调用中完成)?假设多个register<T>()不会引起任何问题。 - Walter
仍然不清楚为什么您会想要在创建之前单独进行类型注册。您能提供一个简单的用例,说明这是最惯用的解决方案吗? - Walter
是的,我可以努力更新我的答案以使其更清晰。我的经理确保每个派生类都有一个唯一的ID和唯一的字符串表示,这有助于数据的序列化和反序列化。 - wesanyer
2个回答

4

在我看来,你有一个设计问题。事实上,registerDerived<Derived>()是调用createDerived<Derived>()的前提条件,这一点应该在代码中表达出来(而不仅仅是在文档中),以便于防止未注册的创建。

实现此目标的一种方法是通过注册文档,在注册时发放并在创建时要求使用。例如:

#include <typeinfo>
#include <typeindex>
#include <unordered_set>

struct Manager {

    // serves as registration document
    template<typename T>
    class ticket { friend struct Manager; };

    // use SFINAE to restrict T to a derived class (assumed C++14)
    template<typename T>
    std::enable_if_t<std::is_base_of<Manager,T>::value, ticket<T> >
    registerDerived()
    {
        registeredTypes.insert(std::type_index(typeid(T)));
        return {};
    }

    template<typename T, typename... Args>
    T createDerived(ticket<T>, Args&&...args)
    {
        return T(std::forward<Args>(args)...);
    }

  private:
    std::unordered_set<std::type_index> registeredTypes;
};

struct Derived1 : Manager {};
struct Derived2 : Manager { Derived2(int); }

int main() {
    Manager manager;
    auto t1 = manager.registerDerived<Derived1>();
    auto t2 = manager.registerDerived<Derived2>();
    auto x1 = manager.createDerived(t1);
    auto x2 = manager.createDerived(t2,7);
}

请注意,对象t很可能已被优化掉。
当然,这段代码与您的代码不同,因为它需要携带ticket<Derived>来创建任何内容。但是,在此简单示例中,注册后创建的概念并不明智,因为以下代码始终可以工作并且无需事先注册(请参阅我在评论中的问题)。
template<typename T, typename...Args>
T Manager::create(Args&&..args)
{
    return createDerived(register<T>(),std::forward<Args>(args)...);
}

如果仅考虑注册本身的成本比我上面简单的例子要高,那么你可以使用上述的unordered_set<type_index>来检查一个Derived类型是否已被注册,再尝试进行注册。

2

我认为以一种可移植/可靠的方式做到这一点是不可能的。

如果你对编译时仅注册感兴趣,我建议将Manager作为模板类来实现,其中模板参数是已注册的类型。

我的意思是...如果你编写一个自定义类型特征如下

template <typename, typename ...>
struct typeInList;

template <typename T0, typename T1, typename ... Ts>
struct typeInList<T0, T1, Ts...> : public typeInList<T0, Ts...>
 { };

template <typename T0, typename ... Ts>
struct typeInList<T0, T0, Ts...> : public std::true_type
 { using type = T0; };

template <typename T0>
struct typeInList<T0> : public std::false_type
 { };

template <typename ... Ts>
using typeInList_t = typename typeInList<Ts...>::type;

或者(如Deduplicator所建议的(感谢!))以更紧凑的方式。
// ground case: in charge only when `typename...` variadic list
// is empy; other cases covered by specializations
template <typename, typename...>
struct typeInList : public std::false_type
 { };

template <typename T0, typename T1, typename ... Ts>
struct typeInList<T0, T1, Ts...> : public typeInList<T0, Ts...>
 { };

template <typename T0, typename ... Ts>
struct typeInList<T0, T0, Ts...> : public std::true_type
 { using type = T0; };

template <typename ... Ts>
using typeInList_t = typename typeInList<Ts...>::type;

您可以使用它来SFINAE启用/禁用createDerived()如下所示

template <typename ... Ts>
struct Manager
 {
   template <typename T>
   typeInList_t<T, Ts...> createDerived ()
    { return T(); }
 };

hasRegisterDerivedMethod 可以如下编写:

template <typename, typename>
struct hasRegisterDerivedMethod;

template <typename ... Ts, typename T>
struct hasRegisterDerivedMethod<Manager<Ts...>, T>
   : public typeInList<T, Ts...>
 { };

不幸的是,这个解决方案只在编译时起作用,而不是在运行时起作用。如果您需要一个既可以在编译时又可以在运行时起作用的解决方案,那么这个解决方案并不适合您。

以下是一个完整的工作示例。

#include <iostream>

template <typename, typename ...>
struct typeInList;

template <typename T0, typename T1, typename ... Ts>
struct typeInList<T0, T1, Ts...> : public typeInList<T0, Ts...>
 { };

template <typename T0, typename ... Ts>
struct typeInList<T0, T0, Ts...> : public std::true_type
 { using type = T0; };

template <typename T0>
struct typeInList<T0> : public std::false_type
 { };

template <typename ... Ts>
using typeInList_t = typename typeInList<Ts...>::type;

template <typename ... Ts>
struct Manager
 {
   template <typename T>
   typeInList_t<T, Ts...> createDerived ()
    { return T(); }
 };

struct IBase { };
struct Derived1 : public IBase{ };
struct Derived2 : public IBase{ };


template <typename, typename>
struct hasRegisterDerivedMethod;

template <typename ... Ts, typename T>
struct hasRegisterDerivedMethod<Manager<Ts...>, T>
   : public typeInList<T, Ts...>
 { };

int main ()
 {
   Manager<Derived1> myManager;
   // whoops, forgot to register Derived2!

   Derived1 d1 = myManager.createDerived<Derived1>();

   //Derived2 d2 = myManager.createDerived<Derived2>(); // compilation error!

   std::cout << std::boolalpha;

   std::cout << "Derived1 check = "
      << hasRegisterDerivedMethod<decltype(myManager), Derived1>::value
      << std::endl; // print true

   std::cout << "Derived2 check = "
      << hasRegisterDerivedMethod<decltype(myManager), Derived2>::value
      << std::endl; // print false
 }

离题:与其

static const bool value = std::is_same<std::true_type, decltype(test<T>(nullptr))>::value;

您可以编写代码

static constexpr bool value { type::value };

感谢您详细的回复。您能否指向类似于 这个维基 的资源,详细说明您正在使用的一些技术?我对所有这些模板感到有些不适应,我想确保我理解解决方案是做什么的,而不是盲目地使用它。 - wesanyer
你的 typeInList 定义为什么要包含一个前向声明和三个特化版本,而不是基本情况(失败)后面跟着两个特化版本(成功和递归)?此外,对于结构体的基类来说,显式的 public 是多余的。 - Deduplicator
@Deduplicator - 或许有更好的编写方式,但是...三个专业领域中没有一个可以被用作通用(非专业化)版本:基本情形只有一个参数(而其他情况至少需要两个);其他两种情况需要两个参数(而基本情形只需要一个)。我知道public是默认的,因此多余,但我认为明确表达更清晰。 - max66
@wesanyer - 不好意思,我不知道有类似的资源涵盖它;但你尝试了解自己在使用什么是对的。我想在自定义类型特性typeInList中难以理解的部分。我建议搜索"模板特化"。 - max66
@max66:失败情况是一个很好的基本情况,只需忽略理论尾巴即可。然后这两个特化会处理它为空的情况。参见:template <class, class...> struct in_list : std::false_type {}; template <class T, class... Ts> struct in_list<T, T, Ts...> : std::true_type { using type = T; }; template <class T, class U, class... Ts> struct in_list<T, U, Ts...> : in_list<T, Ts...> {}; - Deduplicator
显示剩余2条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接