Проблема в том, что вы нарушили одно правило определения. В main.C вы включили Foo.H, но не Foo.C (что имеет смысл, поскольку это исходный файл). Когда main.C компилируется, компилятор не знает, что вы специализировали шаблон в Foo.C, поэтому он использует универсальную версию (которая возвращает 6) и компилирует класс Foo. Затем, когда он компилирует Foo.C, он видит полную специализацию, которую он может скомпилировать сразу - ему не нужно ждать, пока он где-нибудь будет создан, потому что все типы заполнены (если вы у него было два параметра шаблона и только специализированный (это было бы не так), и он компилирует новый и отличный класс Foo.
Обычно множественные определения для одной и той же вещи вызывают ошибку компоновщика. Но экземпляры шаблона являются «слабыми символами», что означает, что допускается несколько определений. Линкер предполагает, что все определения действительно одинаковы , а затем выбирает одно случайное (ну, вероятно, последовательно первое или последнее, но только как совпадение реализации).
Зачем делать их слабыми символами? Поскольку Foo может использоваться в нескольких исходных файлах, каждый из которых компилируется индивидуально, и каждый раз, когда Foo используется в модуле компиляции, генерируется новый экземпляр. Обычно они избыточны, поэтому имеет смысл выбросить их. Но вы нарушили это предположение, предоставив специализацию в одном модуле компиляции (foo.C), но не в другом (main.C).
Если вы объявляете специализацию шаблона в Foo.H, то когда main.C скомпилирован, он не генерирует экземпляр Foo, таким образом гарантируя, что в вашей программе существует только одно определение.