Avatar ·

Как правильно хранить определения шаблонных функций в .cpp файле в C++?

Привет! Я изучаю C++ и столкнулся с проблемой, которую не могу решить уже второй день. Пишу небольшой класс-контейнер на шаблонах. Решил разделить объявление и реализацию по разным файлам, как обычно делаю с обычными классами. Вот пример: ```cpp // MyArray.h template class MyArray { private: T* data; int size; public: MyArray(int s); ~MyArray(); T& operator[](int index); }; // MyArray.cpp #include "MyArray.h" template MyArray::MyArray(int s) : size(s) { data = new T[size]; } template MyArray::~MyArray() { delete[] data; } template T& MyArray::operator[](int index) { return data[index]; } // main.cpp #include "MyArray.h" int main() { MyArray arr(10); arr[0] = 42; return 0; } ``` При компиляции получаю ошибки линковки: "undefined reference to `MyArray::MyArray(int)'" и аналогичные для других методов. В интернете пишут, что это связано с тем, как работают шаблоны, но я не понимаю, почему обычные классы работают нормально, а шаблонные — нет. Объясните, пожалуйста, в чём дело и как правильно организовать код. Компилятор MSVC, если это важно. <<>>

Почему возникает ошибка линковки?

Ключевая проблема в том, что шаблоны в C++ — это не обычный код, который компилируется сразу. Шаблон — это "инструкция для компилятора" по генерации кода. Когда вы пишете template<typename T> class MyArray {...}, компилятор не создаёт никакого кода, пока не увидит конкретное использование шаблона с конкретным типом (например, MyArray<int>).

В вашем случае:

  • MyArray.cpp компилируется в объектный файл. Но в нём нет ни одного явного использования шаблона с конкретным типом. Компилятор просто проверяет синтаксис, но не генерирует код для MyArray<int>.
  • main.cpp видит только заголовок MyArray.h. Он знает, что нужно создать MyArray<int>, но не имеет реализации методов — она лежит в .cpp файле, который не включён.
  • На этапе линковки компоновщик ищет реализации методов MyArray<int> в объектных файлах, но не находит — их никто не сгенерировал.

С обычными (нешаблонными) классами такой проблемы нет, потому что их методы компилируются сразу в объектный файл и доступны линковщику.

Решение 1: Перенести реализацию в заголовочный файл (самое популярное)

Самый простой и рекомендуемый способ — поместить определения шаблонных методов прямо в .h файл:

// MyArray.h
template<typename T>
class MyArray {
private:
    T* data;
    int size;
public:
    MyArray(int s) : size(s) {
        data = new T[size];
    }
    ~MyArray() {
        delete[] data;
    }
    T& operator[](int index) {
        return data[index];
    }
};

Теперь каждый .cpp файл, который включает MyArray.h и использует MyArray<int>, сам сгенерирует нужный код. Никаких ошибок линковки.

Решение 2: Явное инстанцирование шаблона (оставляем .cpp)

Если вы обязательно хотите держать реализацию в .cpp (например, для сокрытия деталей или ускорения компиляции), нужно явно указать компилятору, для каких типов генерировать код. Добавьте в конец MyArray.cpp:

// MyArray.cpp
#include "MyArray.h"

// ... реализации методов ...

// Явное инстанцирование для нужных типов
template class MyArray<int>;
template class MyArray<double>;
// Добавьте все типы, которые используете

Теперь объектный файл MyArray.obj будет содержать готовый код для MyArray<int> и MyArray<double>. Линковщик найдёт их.

Недостаток: вы должны заранее знать все типы, с которыми будет использоваться шаблон. Если пользователь вашей библиотеки захочет MyArray<std::string> — снова ошибка.

Решение 3: Файл ".tpp" или ".inl" (промежуточное)

Можно вынести реализации в отдельный файл с расширением .tpp или .inl и включить его в конце заголовка:

// MyArray.h
template<typename T>
class MyArray {
    // объявления...
};

#include "MyArray.tpp"  // реализация в конце

Это даёт визуальное разделение, но технически всё равно всё оказывается в заголовке.

Вывод

Для новичков и большинства проектов используйте первый способ (реализация в заголовке). Это стандартная практика для шаблонов в C++. Явное инстанцирование применяют только в больших проектах для сокращения времени компиляции или в библиотеках с фиксированным набором типов.

<<>>

Альтернативный подход: экспорт шаблонов (C++20 modules)

Если вы используете современный компилятор (C++20 и новее), можно применить модули. Они решают проблему разделения объявления и реализации шаблонов более элегантно:

// MyArray.ixx (или .cppm)
export module MyArray;

export template<typename T>
class MyArray {
private:
    T* data;
    int size;
public:
    MyArray(int s);
    ~MyArray();
    T& operator[](int index);
};

// Реализация прямо здесь или в отдельном файле модуля
template<typename T>
MyArray<T>::MyArray(int s) : size(s) {
    data = new T[size];
}
// ... остальные методы

Теперь в main.cpp:

import MyArray;

int main() {
    MyArray<int> arr(10);
    arr[0] = 42;
}

Плюсы:

  • Нет проблем с ODR (One Definition Rule).
  • Модули компилируются один раз, а не копируются в каждый .cpp.
  • Лучшая изоляция: внутренние детали не видны пользователю.

Минусы:

  • Требуется C++20 и поддержка компилятором (MSVC поддерживает, GCC/Clang — частично).
  • Меньше примеров и учебных материалов.

Этот метод пока не стал массовым, но это будущее C++. Если ваш проект позволяет, попробуйте модули — они решают много старых проблем с шаблонами.