Q:

Is it safe to use the address of a static local variable within a function template as a type identifier?

I wish to create an alternative to std::type_index that does not require RTTI:
template <typename T>
int* type_id() {
    static int x;
    return &x;
}

Note that the address of the local variable x is used as the type ID, not the value of x itself. Also, I don't intend to use a bare pointer in reality. I've just stripped out everything not relevant to my question. See my actual type_index implementation here.
Is this approach sound, and if so, why? If not, why not? I feel like I am on shaky ground here, so I am interested in the precise reasons why my approach will or will not work.
A typical use case might be to register routines at run-time to handle objects of different types through a single interface:
class processor {
public:
    template <typename T, typename Handler>
    void register_handler(Handler handler) {
        handlers[type_id<T>()] = [handler](void const* v) {
            handler(*static_cast<T const*>(v));
        };
    }

    template <typename T>
    void process(T const& t) {
        auto it = handlers.find(type_id<T>());
        if (it != handlers.end()) {
            it->second(&t);
        } else {
            throw std::runtime_error("handler not registered");
        }
    }

private:
    std::map<int*, std::function<void (void const*)>> handlers;
};

This class might be used like so:
processor p;

p.register_handler<int>([](int const& i) {
    std::cout << "int: " << i << "\n";
});
p.register_handler<float>([](float const& f) {
    std::cout << "float: " << f << "\n";
});

try {
    p.process(42);
    p.process(3.14f);
    p.process(true);
} catch (std::runtime_error& ex) {
    std::cout << "error: " << ex.what() << "\n";
}

Conclusion
Thanks to everyone for your help. I have accepted the answer from @StoryTeller as he has outlined why the solution should be valid according the rules of C++. However, @SergeBallesta and a number of others in the comments have pointed out that MSVC performs optimizations which come uncomfortably close to breaking this approach. If a more robust approach is needed, then a solution using std::atomic may be preferable, as suggested by @galinette:
std::atomic_size_t type_id_counter = 0;

template <typename T>
std::size_t type_id() {
    static std::size_t const x = type_id_counter++;
    return x;
}

If anyone has further thoughts or information, I am still eager to hear it!

A:

Yes, it will be correct to an extent. Template functions are implicitly inline, and static objects in inline functions are shared across all translation units. 
So, in every translation unit, you will get the address of the same static local variable for the call to type_id<Type>(). You are protected here from ODR violations by the standard.
Therefore, the address of the local static can be used as a sort of home-brewed run-time type identifier.

A:

This is coherent with standard because C++ use templates and not generics with type erasure like Java so each declared type will have its own function implementation containing a static variable. All those variables are different and as such should have different addresses.
The problem is that their value is never used and worse never changed. I remember that the optimizers can merge string constants. As optimizers do their best to be far more clever than any human programmer, I will be afraid that a too zealous optimizing compiler discover that as those variable values are never changed, they will all keep a 0 value, so why not merge them all to save memory?
I know that because of the as if rule, the compiler is free to do what it wants provided the observable results are the same. And I am not sure that the addresses of static variables that will always share the same value shall be different or not. Maybe someone could confirm what part of the standard actually cares for it?
Current compilers still compile separately program units, so they cannot be sure whether another program unit will use or change the value. So my opinion is that the optimizer will not have enough information to decide to merge the variable, and your pattern is safe.
But as I really do not think that standard protects it, I cannot say whether future versions of C++ builders (compiler + linker) will not invent a global optimizing phase actively searching for unchanged variables that could be merged. More or less the same as they actively search UB to optimize out parts of code... Only common patterns, where not allowing them would break a too large code base are protected of it, and I do not think that yours is common enough.
A rather hacky way to prevent an optimizing phase to merge variables having same value would just be to give each one a different value:
int unique_val() {
    static int cur = 0;  // normally useless but more readable
    return cur++;
}
template <typename T>
void * type_id() {
    static int x = unique_val();
    return &x;
}

Ok, this does not even try to be thread safe, but it not a problem here: the values will never be used per themselves. But you now have different variables having static duration (per 14.8.2 of standard as said by @StoryTeller), that except in race conditions have different values. As they are odr used they must have different addresses and you should be protected for future improvement of optimizing compilers...
Note: I think that as the value will not be used, returning a void * sounds cleaner...

Just an addition stolen from a comment from @bogdan. MSVC is known to have very aggressive optimization with the /OPT:ICF flag. The discussion suggest that is should not be conformant, and that it only applies to variable marked as const. But it enforces my opinion that even if OP's code seems conformant, I would not dare to use it without additional precautions in production code.

A:

Post-comment edit : I did not realize at first read that the address was used as the key, not the int value. That's a clever approach, but it suffers IMHO a major flaw : the intent is very unclear if someone else finds that code.
It looks like an old C hack. It's clever, efficient, but the code does not self-explain at all what the intent is. Which in modern c++, imho, is bad. Write code for programmers, not for compilers. Unless you have proven that there is a serious bottleneck which requires bare metal optimization.
I would say it should work but I'm clearly not a language lawyer...
An elegant, but complex constexpr solution, may be found here or here
Original answer
It is "safe" in the sense that this is valid c++ and you can access the returned pointer in all your program, as the static local will be initialized at first function call. There will be one static variable per type T used in your code.
But :

Why returning a non const pointer? This will allow callers to change the static variable value, which is clearly not something you would like
If returning a const pointer, I see no interest in not returning by value instead of returning the pointer

Also, this approach for getting a type id will only work at compile time, not at run time with polymorphic objects. So it will never return the derived class type from a base reference or pointer.
How will you initialize the static int values? Here you do not initialize them so this is not valid. Maybe you wanted to use the non const pointer for initializing them somewhere?
There are two better possibilities:
1)Specialize the template for all the types you want to support
template <typename T>
int type_id() {
    static const int id = typeInitCounter++;
    return id;
}

template <>
int type_id<char>() {
    static const int id = 0;
    return id;  //or : return 0
}

template <>
int type_id<unsigned int>() {
    static const int id = 1;
    return id;  //or : return 1
}

//etc...

2)Use a global counter
std::atomic<int> typeInitCounter = 0;

template <typename T>
int type_id() {
    static const int id = typeInitCounter++;
    return id;
}

This last approach is IMHO better because you don't have to manage types. And as pointed out by A.S.H, zero-based incremented counter allows using a vector instead of a map which is much more simple and efficient.
Also, use an unordered_map instead of a map for this, you do not need ordering. This gives you O(1) access instead of O(log(n))

