Подтвердить что ты не робот

Функция шаблона С++ компилируется в заголовке, но не выполняется

Я пытаюсь изучить шаблоны, и я столкнулся с этой ошибочной ошибкой. Я объявляю некоторые функции в файле заголовка, и я хочу создать отдельный файл реализации, в котором будут определены функции. Здесь код, вызывающий заголовок (dum.cpp):

#include <iostream>
#include <vector>
#include <string>
#include "dumper2.h"

int main() {
    std::vector<int> v;
    for (int i=0; i<10; i++) {
        v.push_back(i);
    }
    test();
    std::string s = ", ";
    dumpVector(v,s);
}

Теперь вот рабочий файл заголовка (dumper2.h):

#include <iostream>
#include <string>
#include <vector>

void test();

template <class T> void dumpVector( std::vector<T> v,std::string sep);

template <class T> void dumpVector(std::vector<T> v, std::string sep) {
    typename std::vector<T>::iterator vi;

    vi = v.begin();
    std::cout << *vi;
    vi++;
    for (;vi<v.end();vi++) {
        std::cout << sep << *vi ;
    }
    std::cout << "\n";
    return;
}

С реализацией (dumper2.cpp):

#include <iostream>
#include "dumper2.h"

void test() {
    std::cout << "!olleh dlrow\n";
}

Странно то, что если я переведу код, определяющий dumpVector из .h в .cpp файл, я получаю следующую ошибку.

g++ -c dumper2.cpp -Wall -Wno-deprecated
g++ dum.cpp -o dum dumper2.o -Wall -Wno-deprecated
/tmp/ccKD2e3G.o: In function `main':
dum.cpp:(.text+0xce): undefined reference to `void dumpVector<int>(std::vector<int, std::allocator<int> >, std::basic_string<char, std::char_traits<char>, std::allocator<char> >)'
collect2: ld returned 1 exit status
make: *** [dum] Error 1

Так почему это работает в одном направлении, а не в другом? Очевидно, что компилятор может найти test(), поэтому почему он не может найти dumpVector?

4b9b3361

Ответ 1

Проблема, с которой вы сталкиваетесь, заключается в том, что компилятор не знает, какие версии вашего шаблона должны создаваться. Когда вы перемещаете реализацию своей функции на x.cpp, она находится в другой единицы перевода от main.cpp, а main.cpp не может ссылаться на конкретный экземпляр, потому что он не существует в этом контексте. Это хорошо известная проблема с шаблонами С++. Существует несколько решений:

1) Просто поместите определения непосредственно в файл .h, как и раньше. У этого есть плюсы и минусы, включая решение проблемы (pro), возможно, делая код менее удобочитаемым, а некоторым компиляторам сложнее отлаживать (con) и, возможно, увеличивать код bloat (con).

2) Поместите реализацию в x.cpp и #include "x.cpp" из x.h. Если это кажется напуганным и неправильным, просто имейте в виду, что #include делает не что иное, как чтение указанного файла и компиляцию его, как если бы этот файл был частью x.cpp. Другими словами, это делает именно то, что решение # 1 делает выше, но он хранит их в отдельных физических файлах. Когда вы делаете подобные вещи, очень важно, чтобы вы не пытались скомпилировать файл #include d. По этой причине я обычно даю такие файлы расширению hpp, чтобы отличать их от файлов h и от cpp файлов.

Файл: dumper2.h

#include <iostream>
#include <string>
#include <vector>

void test();
template <class T> void dumpVector( std::vector<T> v,std::string sep);
#include "dumper2.hpp"

Файл: dumper2.hpp

template <class T> void dumpVector(std::vector<T> v, std::string sep) {
  typename std::vector<T>::iterator vi;

  vi = v.begin();
  std::cout << *vi;
  vi++;
  for (;vi<v.end();vi++) {
    std::cout << sep << *vi ;
  }
  std::cout << "\n";
  return;

}

3) Поскольку проблема заключается в том, что определенная реализация dumpVector не известна блоку трансляции, который пытается ее использовать, вы можете принудительно установить его конкретную копию в той же самой системе перевода, что и в случае, когда шаблон определен, Просто добавив следующее: template void dumpVector<int>(std::vector<int> v, std::string sep);... в файл, где шаблон определен. Для этого вам больше не нужно #include файла hpp из файла h:

Файл: dumper2.h

#include <iostream>
#include <string>
#include <vector>

void test();
template <class T> void dumpVector( std::vector<T> v,std::string sep);

Файл: dumper2.cpp

template <class T> void dumpVector(std::vector<T> v, std::string sep) {
  typename std::vector<T>::iterator vi;

  vi = v.begin();
  std::cout << *vi;
  vi++;
  for (;vi<v.end();vi++) {
    std::cout << sep << *vi ;
  }
  std::cout << "\n";
  return;
}

template void dumpVector<int>(std::vector<int> v, std::string sep);

Кстати, и в целом в стороне ваша функция шаблона принимает значение vector. Возможно, вы не захотите это сделать и передать его по ссылке или указателю или, еще лучше, передать итераторы вместо этого, чтобы избежать временного копирования и копирования всего вектора.

Ответ 2

Это то, что должно было выполнить ключевое слово export (т.е. export с шаблоном, вы могли бы поместить его в исходный файл вместо заголовка. К сожалению, только один компилятор (Comeau), когда-либо реально реализованный export полностью.

Что касается того, почему другие компиляторы (включая gcc) не реализовали его, причина довольно проста: потому что export чрезвычайно сложно реализовать правильно. Код внутри шаблона может полностью изменить значение (почти), основываясь на типе, в котором создается экземпляр шаблона, поэтому вы не можете создать обычный объектный файл результата компиляции шаблона. Например, x+y может скомпилировать собственный код типа mov eax, x/add eax, y при создании экземпляра int, но скомпилировать его для вызова функции, если экземпляр произошел над чем-то вроде std::string, который перегружает operator+.

Чтобы поддерживать отдельную компиляцию шаблонов, вы должны делать то, что называется двухфазным поиском имен (т.е. искать имя как в контексте шаблона, так и в контексте, где создается экземпляр шаблона). Обычно у вас также есть компилятор, который компилирует шаблон в какой-то формат базы данных, который может содержать экземпляры шаблона по произвольной коллекции типов. Затем вы добавляете этап между компиляцией и связыванием (хотя он может быть встроен в компоновщик, если это необходимо), который проверяет базу данных, и если он не содержит кода для шаблона, созданного по всем необходимым типам, повторно вызывает компилятор для его создания над необходимыми типами.

Из-за крайних усилий, отсутствия реализации и т.д. комитет проголосовал за удаление export из следующей версии стандарта С++. Были сделаны два других, довольно разных предложения (модулей и концепций), которые будут обеспечивать, по меньшей мере, часть того, что должно было делать export, но способами, которые (по крайней мере, надеются) быть более полезными и разумными для реализации.

Ответ 3

Параметры шаблона разрешены как время компиляции.

Компилятор находит .h, находит соответствующее определение для dumpVector и сохраняет его. Компиляция завершена для этого .h. Затем он продолжает анализировать файлы и компилировать файлы. Когда он читает реализацию dumpVector в .cpp, он компилирует совершенно другую единицу. Ничто не пытается создать шаблон в dumper2.cpp, поэтому код шаблона просто пропущен. Компилятор не будет пытаться использовать все возможные типы для шаблона, надеясь, что позже будет добавлено что-то полезное для компоновщика.

Затем во время ссылки никакая реализация dumpVector для типа int не была скомпилирована, поэтому компоновщик не найдет. Следовательно, почему вы видите эту ошибку.

export ключевое слово предназначено для решения этой проблемы, к сожалению, немногие компиляторы поддерживают его. Поэтому сохраните свою реализацию с тем же файлом, что и ваше определение.

Ответ 4

Функция шаблона не является реальной функцией. Компилятор превращает функцию шаблона в реальную функцию, когда он сталкивается с использованием этой функции. Таким образом, объявление всего шаблона должно быть в области видимости, оно находит вызов DumpVector, в противном случае он не может генерировать реальную функцию.
Удивительно, но многие интро-книги С++ ошибаются.

Ответ 5

Именно так работают шаблоны на С++, вы должны поместить реализацию в заголовок.

Когда вы объявляете/определяете функцию шаблона, компилятор не может волшебным образом узнать, какие конкретные типы вы можете использовать с шаблоном, поэтому он не может сгенерировать код для размещения в файле .o, как это было бы возможно с помощью нормальная функция. Вместо этого он полагается на генерирование определенного экземпляра для типа, когда видит использование этого экземпляра.

Итак, когда реализация находится в файле .C, компилятор в основном говорит: "Эй, нет пользователей этого шаблона, не генерируйте никакого кода". Когда шаблон находится в заголовке, компилятор может видеть использование в основном и фактически генерировать соответствующий код шаблона.