Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How can I check if a templated method was called at compile-time?

I am writing an entity entity component system game engine. As a part of this, I have written a Manager class which will register various IBase implementations and, later, allow me to instantiate these implementation. See below for an example of how I wish to use this.

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{
};

As noted in the comments, I have code in template<class T>Manager::createDerived() which checks whether or not a particular implementation of Base has been registered using template<class T>Manager::registerDerived(), and if it has not been registered it throws an error. This check is trivial and was left out of the code sample to keep things simple.

Here is my question: is it possible to move this check to compile-time, rather than waiting until runtime? It seems like there should be enough information at runtime to make this determination.

So far, I've explored/read about SFINAE, which seems like the approach to take, but I cannot figure out how to make these idioms work in this specific case. This link gives a good overview of the basic SFINAE idiom, this SO question gives some good code snippets, and finally This blog post seems to address almost my exact situation.

Here is a full example which is my attempt to implement the information found in these links:

#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;
}

**

TL;DR

How can I modify the code above to produce a compile-time error (probably using static_assert) instead of waiting until runtime to detect the error?

**

like image 743
wesanyer Avatar asked Aug 25 '18 16:08

wesanyer


2 Answers

IMHO, you have a design issue. The fact that registerDerived<Derived>() is a prerequisite for a call to createDerived<Derived>() should be expressed in code (not merely in the documentation), such that unregistered creation is impossible.

One way to achieve this is via a registration document, issued at registration and required at creation. For example

#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);
}

Note that the object t is likely optimized away.

Of course, this code is different from yours, as it requires to carry the ticket<Derived> around for any creation. However, the very concept of registering followed by creation is not sensible in this simple example, as the following code would always work and do w/o prior registration (see also my question in the comments):

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

If registration per see is a more expensive process than in my simple example, then you may check (using a unordered_set<type_index> as above) whether a Derived type is registered before attempting to do so.

like image 198
Walter Avatar answered Sep 19 '22 13:09

Walter


I don't think it's possible in a portable/reliable way.

If your interested in compile-time only registration, I suggest to make Manager a template class where the template parameters are the registered types.

I mean... if you write a custom type-traits as follows

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;

or (as suggested by Deduplicator (thanks!)) in a more compact way

// 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;

you can use it to SFINAE enable/disable createDerived() as follows

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

and hasRegisterDerivedMethod can be written as follows

template <typename, typename>
struct hasRegisterDerivedMethod;

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

Unfortunately this works compile-time but not run-time so, if you need a solution that works both compile-time and run-time, this solution isn't for you.

The following is a full working example

#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
 }

Off Topic: instead of

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

you can write

static constexpr bool value { type::value };
like image 24
max66 Avatar answered Sep 21 '22 13:09

max66