Перейти к основному содержимому

Специализация шаблона и дублирование символа. Встроенные функции и шаблоны

· 5 мин. чтения
Дмитрий Аладин
Преподаватель

В данной статье разобраны причины возникновения таких ошибок, как error LNK1169: one or more multiply defined symbols found (MSVC), error LNK2005 with detail description of defining it twice (MSVC) и duplicate symbol (GCC, Clang/LLVM) при использовании специализации шаблона.

Исходный проект

Предположим, что имеется проект, состоящий из файлов foo.h, awesome.h, awesome.cpp и main.cpp.

Содержимое foo.h:

#ifndef FOO_H
#define FOO_H

#include <iostream>

template <typename T>
class Foo {
T value;

public:
Foo() : value() {}
explicit Foo(const T t_value) : value(t_value) {}
void print() { std::cout << "General output: " << value << std::endl; }
};

template <>
void Foo<std::string>::print() {
std::cout << "String output: " << value << std::endl;
}

#endif

Содержимое awesome.h:

#ifndef AWESOME_H
#define AWESOME_H
#include "foo.h"

class Awesome {
Foo<std::string> f;

public:
Awesome();
explicit Awesome(std::string value);
void print();
};
#endif

Содержимое awesome.cpp:

#include "awesome.h"

Awesome::Awesome() : f("") {}

Awesome::Awesome(std::string value) : f(value) {}

void Awesome::print() { f.print(); }

Содержимое main.cpp:

#include <iostream>

#include "awesome.h"
#include "foo.h"

int main(int, char**) {
Foo<int> foo1(1);
foo1.print(); // General output: 1
Foo<std::string> foo2("z"); // String output: z
foo2.print();

Awesome a("s");
a.print(); // String output: s
}

При попытке собрать данный проект с помощью CMake используя Clang для macOS, возникнет следующая ошибка:

[build] duplicate symbol '__ZN3FooINSt3__112basic_stringIcNS0_11char_traitsIcEENS0_9allocatorIcEEEEE5printEv' in:
[build] /project/build/CMakeFiles/project.dir/awesome.cpp.o
[build] /project/build/CMakeFiles/project.dir/main.cpp.o
[build] ld: 1 duplicate symbols
[build] clang: error: linker command failed with exit code 1 (use -v to see invocation)
[build] ninja: build stopped: subcommand failed.

Данная ошибка указывает на то, что при сборке был скомпилирован шаблонный класс Foo<std::string> в нескольких местах, а именно из файлов исходных кодов awesome.cpp и main.cpp. Линковщик (он же компоновщик, линкер) не знает о том, какую именно реализацию шаблона класса использовать (о том, как работает линковщик, можно узнать в лекции). Если же убрать специализацию шаблона template <> void Foo<std::string>::print(), то проект успешно будет собран (о том, как работает компилятор при компиляции шаблонов, можно узнать в лекции).

Объяснение проблемы

Для объяснения причины возникновения данной ошибки, обратим внимание на то, что говорится о шаблонах в рабочих материалах ISO комитета по C++:

Ни одна программа не должна явно создавать экземпляр какого-либо шаблона более одного раза, как явно создавать экземпляр, так и явно специализировать шаблон, или специализировать шаблон более одного раза для данного набора шаблон-аргументов.

Этот параграф полностью удовлетворяет правилу одного определения (ODR): в программе не может появиться два или более конфликтующих между собой определения одной и той же сущности.

Неправильное решение

Если начать искать решение проблемы на stackoverflow, то можно наткнуться на следующее предложение.

... вы можете определить специализации в файле .cpp (но без чистого шаблонного кода и не включать его!), либо вы можете встроить их и сохранить в заголовке.

С предложением переместить определение специализацию шаблона в .cpp и объявить их .h на других форумах.

Объявлять специализацию в файле .cpp может быть плохим решением. Развернутый ответ о том, почему это не следует делать, представлен в ответе на stackoverflow.

Если обратиться к рабочим материалам комитета ISO, то можно обнаружить следующее:

Если шаблон, шаблон-член или элемент шаблона класса явно специализирован, то эта специализация должна быть объявлена до первого использования этой специализации, что приведет к неявному созданию экземпляра, в каждой единице трансляции, в которой встречается такое использование; диагностика не требуется. Если программа не предоставляет определения для явной специализации, и либо специализация используется таким образом, что это привело бы к неявному созданию экземпляра, либо элемент является виртуальной функцией-членом, программа неправильно сформирована, диагностика не требуется. Неявный экземпляр никогда не создается для явной специализации, которая объявлена, но не определена.

Размещение явных объявлений специализации для шаблонов [...], а также размещение объявлений частичной специализации может повлиять на то, является ли программа хорошо сформированной в соответствии с относительным расположением явных объявлений специализации и их точек инстанцирования в единице трансляции. [...] При написании специализации будьте внимательны к ее расположению, иначе компиляция будет таким испытанием, которое приведет к ее самосожжению.

к сведению

Единица трансляции - максимальный блок исходного текста, который физически можно оттранслировать.

Программа состоит из одного или нескольких единиц трансляции. Единица трансляции состоит из файла реализации и всех заголовков, которые он включает прямо или косвенно.

При использовании .cpp для специализации в некоторых единицах трансляции специализация может быть объявлена до первого использования, а в некоторых единицах трансляции этого не будет.

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

Правильное решение

к сведению

Об inline можно подробно прочитать в статье Inline variables | Хабр

Чтобы решить рассматриваемую проблему, нужно пометить специализацию void Foo<std::string>::print() как inline:

#ifndef FOO_H
#define FOO_H

#include <iostream>

template <typename T>
class Foo {
T value;

public:
Foo() : value() {}
explicit Foo(const T t_value) : value(t_value) {}
void print() { std::cout << "General output: " << value << std::endl; }
};

template <>
void inline Foo<std::string>::print() {
std::cout << "String output: " << value << std::endl;
}

#endif

Из статьи Inline variables | Хабр:

Функции и переменные, объявленные inline, могут быть определены в программе несколько раз.

Это, по сути, исключение из правила одного определения. ODR говорит, что вы можете определить только один раз функции, переменные, классы, перечисление и т. д.

ODR должен выполняться не только на уровне единицы трансляции, но и на уровне программы.

Встроенные функции и переменные являются исключением из правила одного определения: они могут быть определены несколько раз в программе (несколько раз в программе, но только единожды в одной единице трансляции).

При работе предпроцессора в файлы main.cpp и awesome.cpp будет вставлена (определена) специализация Foo<std::string>. Таким образом, в нескольких местах программы можно будет увидеть определение одного и того же символа. Но при компиляции, за счет inline, эта специализация будет транслирована единожды!

Дополнительный материал

  1. How can I avoid linker errors with my template classes? | isocpp.org