Шаблонная функция C ++ компилируется в заголовке, но не в реализации - PullRequest
26 голосов
/ 15 июня 2010

Я пытаюсь выучить шаблоны и столкнулся с этой ошибкой. Я объявляю некоторые функции в заголовочном файле и хочу создать отдельный файл реализации, в котором будут определены функции. Вот код, который вызывает заголовок (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?

Ответы [ 6 ]

36 голосов
/ 15 июня 2010

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

1) Просто поместите определения прямо в файл .h, как вы делали раньше. Это имеет свои плюсы и минусы, в том числе решение проблемы (за), возможно, делает код менее читаемым и на некоторых компиляторах труднее отлаживать (con) и, возможно, увеличивает раздувание кода (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 по значению . Вы можете не захотеть делать это и передавать его по ссылке или по указателю или, что еще лучше, вместо этого передавать итераторы, чтобы избежать создания временного и копирования всего вектора.

10 голосов
/ 15 июня 2010

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

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

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

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

5 голосов
/ 15 июня 2010

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

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

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

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

2 голосов
/ 15 июня 2010

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

1 голос
/ 15 июня 2010

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

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

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

0 голосов
/ 15 июня 2010

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

См. Также:

http://www.parashift.com/c++-faq-lite/templates.html#faq-35.12

http://www.parashift.com/c++-faq-lite/templates.html#faq-35.14

...