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

2

6

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?

**

Audiphone answered 25/8, 2018 at 16:29 Comment(5)
I'm afraid, this will not be possible at compile time. You cannot attach a "state" to a non-constexpr object at compile-time and read that state at compile time too.Santanasantayana
Is Manager a singleton?Hysteroid
What is wrong with replacing create() by register() followed by create() (but done in one function call)? Assuming that multiple register<T>() don't cause any issues.Hysteroid
It remains unclear why you would ever want to do this (type registration separately from and prior to creation). Can you provide a simple use case, where this is the most idiomatic solution?Hysteroid
yes I can work to update my answer to make this more clear. my manager ensures that each Derived has a unique ID as well as a unique string representation, which aids with (de) serialization of the dataAudiphone
H
4

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.

Hysteroid answered 26/8, 2018 at 8:46 Comment(0)
E
2

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) 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 };
Edmead answered 25/8, 2018 at 17:28 Comment(7)
Thanks for the detailed response. Can you point me to any resources similar to this wiki which details some of the techniques you are using? I feel a bit out of my element with all these templates and I'd like to make sure I understand what the solution is doing rather than blindly using it.Audiphone
Any reason your definition of typeInList consists of a forward-declaration and 3 specializations, instead of the base-case (failure), followed by two specializations (success and recurse)? Also, explicit public for a struct's base is superfluous.Poseidon
@Poseidon - maybe it's possible write in a better way but... no one of the three specialization can be used as generic (not-specialized) version: the base-case because has only an argument (where the other require at least two); the other two because require two arguments (where the base-case require only one). And I know that public is the default, so superfluous, but I find clearer explicit it.Edmead
@Audiphone - no, sorry: I don't know a similar resources that cover it; but you're right trying to understand what you're using. I suppose the part that is difficult to understand in the custom type-traits typeInList. I suggest to search for "template specialization".Edmead
@max66: The fail-case is a good base-case, just ignore that there is a theoretic tail. The two specializations then take care of it not being empty. See: 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...> {};Poseidon
@Poseidon - Interesting... I had never thought of solving this way. I prefer my solution with forward declaration and three specialization but only because I find it more simple to understand. But I admit that there is an intrinsic elegance in your solution. Added your code as alternative way to write the typeInList type-traits. Thanks.Edmead
@Audiphone - Deduplicator suggested me another way, more compact and (IMHO) more elegant, to write the typeInList type-traits. Maybe it's a little more difficult to understand but I think can be useful, for you, to understand this sort of technique.Edmead

© 2022 - 2024 — McMap. All rights reserved.