(static initialization/template instantiation) pro

2019-02-15 07:11发布

问题:

Why does following code raise an exception (in createObjects call to map::at) alternativly the code (and its output) can be viewed here

intererestingly the code works as expected if the commented lines are uncommented with both microsoft and gcc compiler (see here), this even works with initMap as ordinary static variable instead of static getter.

The only reason for this i can think of is that the order of initialization of the static registerHelper_ object (factory_helper_)and the std::map object (initMap) are wrong, however i cant see how that could happen, because the map object is constructed on first usage and thats in factory_helper_ constructor, so everything should be alright shouldnt it ? I am even more suprised that those doNothing() lines fix the issue, because that call to doNothing() would happen after the critical section (which currently fails) is passed anyway.

EDIT: debugging showed, that without the call to factory_helper_.doNothing(), the constructor of factory_helper_ is never called.

#include <iostream>
#include <string>
#include <map>

#define FACTORY_CLASS(classtype) \
extern const char classtype##_name_[] = #classtype; \
class classtype : FactoryBase<classtype,classtype##_name_>

namespace detail_
{
    class registerHelperBase
    {
    public:
        registerHelperBase(){}
    protected:
        static std::map<std::string, void * (*)(void)>& getInitMap() {
            static std::map<std::string, void * (*)(void)>* initMap = 0;
            if(!initMap)
                initMap= new std::map<std::string, void * (*)(void)>();
            return *initMap;
        }
    };

    template<class TParent, const char* ClassName>
    class registerHelper_ : registerHelperBase {
        static registerHelper_ help_;
    public:
        //void doNothing(){}
        registerHelper_(){
            getInitMap()[std::string(ClassName)]=&TParent::factory_init_;
        }
    };
    template<class TParent, const char* ClassName>
    registerHelper_<TParent,ClassName> registerHelper_<TParent,ClassName>::help_;
}

class Factory : detail_::registerHelperBase
{
private:
    Factory();
public:
    static void* createObject(const std::string& objclassname) {
        return getInitMap().at(objclassname)();
    }
};


template <class TClass, const char* ClassName>
class FactoryBase {
    private:
        static detail_::registerHelper_<FactoryBase<TClass,ClassName>,ClassName> factory_helper_;
        static void* factory_init_(){ return new TClass();}
    public:
        friend class detail_::registerHelper_<FactoryBase<TClass,ClassName>,ClassName>;
        FactoryBase(){
            //factory_helper_.doNothing();
        }
        virtual ~FactoryBase(){};
};

template <class TClass, const char* ClassName>
detail_::registerHelper_<FactoryBase<TClass,ClassName>,ClassName> FactoryBase<TClass,ClassName>::factory_helper_;


FACTORY_CLASS(Test) {
public:
    Test(){}
};

int main(int argc, char** argv) {
    try {
        Test* test = (Test*) Factory::createObject("Test");
    }
    catch(const std::exception& ex) {
        std::cerr << "caught std::exception: "<< ex.what() << std::endl;
    }
    #ifdef _MSC_VER
        system("pause");
    #endif
    return 0;
}

回答1:

The problem is not related to initialization order, but rather to template instantiation.

Templated code is instantiated on demand, that is, the compiler will not instantiate any templated code that is not used in your program. In particular, in your case the static class member FactoryBase<>::factory_helper_ is not being instantiated and thus it does not exist in the final binary, it does not register itself... (you can check this with 'nm' from the gnu toolchain, that will show the list of symbols present in your executable)

Try changing the FactoryBase constructor to this:

template <class TClass, const char* ClassName>
class FactoryBase {
   //...
   FactoryBase(){
      factory_helper_;
   }
   //...
};

This will force the compiler into actually instantiating the static member in the binary and you should be set. There is no need to create an empty method and calling it.

EDIT: As an answer to the comment, towards the end of paragraph §14.7.1[temp.inst]/1 in the current standard:

Unless a member of a class template or a member template has been explicitly instantiated or explicitly specialized, the specialization of the member is implicitly instantiated when the specialization is referenced in a context that requires the member definition to exist; in particular, the initialization (and any associated side-effects) of a static data member does not occur unless the static data member is itself used in a way that requires the definition of the static data member to exist.



回答2:

#define FACTORY_CLASS(classtype) \
class classtype; \
extern const char classtype##_name_[] = #classtype; \
template detail_::registerHelper_<FactoryBase<classtype,classtype##_name_>,classtype##_name_> FactoryBase<classtype,classtype##_name_>::factory_helper_; \
class classtype : FactoryBase<classtype,classtype##_name_>

explicitly instantiating factory_helper_ fixed the issue.