STM32 interrupts and C++ don't go well together
I've been searching for a solution for half a day, only to find it a few minutes after I posted this question. The answer is given by this post:

Bug in Keil ARM compiler with interrupt handlers and C++?


  The "weak" reference just means that the routine will be replaced by a
  routine in your code of the same name. When using C this is simple,
  the names will always be identical but C++ name mangles the functions
  (for function overloading etc) so the compiled name will probably not
  match the default ISR name. You need to wrap the function (or at least
  a forward reference, I'm not sure of specifics I mostly work in C) in
  an extern "C" wrapper to force the compiler to not mangle the name.
  
  extern "C" {   void SysTick_Handler(void)   {
      // do whatever   } }


Or you can just declare it as

extern "C" void SysTick_Handler(void);


So you don't have to wrap it.