LINUX.ORG.RU

Template в разделяемой библиотеке

 , ,


0

2

Не могу понять почему, при использовании внутри разделяемой библиотеки шаблона, я получаю ошибку линковщика «undefined reference».

Разделяемая библиотека состоит из двух классов.

Внешний класс Lib:

lib.h:

#ifndef LIB_H
#define LIB_H

#include "Lib_global.h"

class LIB_EXPORT Lib
{
public:
    Lib();
    void test();
};

#endif // LIB_H

lib.cpp:

#include "lib.h"
#include "stream.h"

Lib::Lib() {}

void Lib::test()
{
    Stream<int> s;
    s.test();
}

Внутренний шаблон класса Stream:

stream.h:

#ifndef STREAM_H
#define STREAM_H

template<class T>
class Stream
{
public:
    Stream();
    void test();
};

#endif // STREAM_H

stream.cpp:

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

template<class T>
Stream<T>::Stream()
{
}

template<class T>
void Stream<T>::test()
{
    std::cout << "stream test" << std::endl;
}

Класс Lib создается в приложении:

main.cpp:

#include <lib.h>

int main()
{
    Lib l;
    l.test();
    return 0;
}

Сборка библиотеки происходит без ошибок. При сборке приложения я получаю ошибку:

g++  -o App main.o   -L/home/sabo/Workspace/TestCppLib/build-TestCppLib-Desktop-Debug/App/../Lib/ -lLib   
/usr/bin/ld: /home/sabo/Workspace/TestCppLib/build-TestCppLib-Desktop-Debug/App/../Lib//libLib.so: undefined reference to `Stream<int>::Stream()'
/usr/bin/ld: /home/sabo/Workspace/TestCppLib/build-TestCppLib-Desktop-Debug/App/../Lib//libLib.so: undefined reference to `Stream<int>::test()'

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

★★

Ответ на: комментарий от bugfixer

Я, видимо, что-то упускаю… Хочу разобраться почему не работает, если метод Lib::test() уже успешно скомпилирован?

М.б. сможете порекомендовать какую-то статью, которая это популярно объясняет?

SaBo ★★
() автор топика
Последнее исправление: SaBo (всего исправлений: 1)
Ответ на: комментарий от SaBo

Хочу разобраться почему не работает, если метод Lib::test() уже успешно скомпилирован?

Скажем так - он проверен на валидность синтаксиса (на сколько это возможно), не более.

Asm появляется в тот момент когда Вы подсовываете template’у конкретные параметры (процесс называется «template instantiation»). Чтобы это произошло - template definition must be visible. Если в этой точке доступен только declaration - компилятор это прожуёт, но скорее всего Вы получите linking error (что у Вас и происходит).

М.б. сможете порекомендовать какую-то статью, которая это популярно объясняет?

Сходу нет, но погуглите «explicit template instantiation». Может это то что Вам нужно (хотя терзают меня смутные сомнения).

ПыСы. Здесь довольно много людей которые смогут это всё объяснить гораздо лучше меня. @fsb4000.

bugfixer ★★★★
()
Ответ на: комментарий от SaBo

Когда ты компилировал stream.cpp, компилятор не знал, что тебе понадобится Stream<int> и поэтому не сделал для него методы. Выяснилось это только при линковке. Библиотека тут ни при чём, ты мог всё в приложении компилировать, результат был бы тот же. Поэтому шаблоны надо либо целиком в .h делать, либо в .cpp файле (в том, где методы для шаблона расположены) заранее указывать для каких типов ты будешь его использовать (explicit instantiation о котором писали выше).

Дописываешь в конец stream.cpp строчку

template class Stream<int>;

и тогда в stream.o и в библиотеке будут методы для Stream<int>.

Минусы способа «шаблон целиком в .h» - компилятор будет его заново компилировать каждый раз, когда ты его куда-то инклюдишь, что плохо скажется на времени компиляции (ну как и происходит обычно).

Минусы способа «explicit instantiation» - тебе надо заранее знать, какие варианты шаблона будут нужны, и вручную поддерживать этот список в соответствующем .cpp файле. Понятное дело, распространять такую библиотеку в скомпилированном виде для использования в других проектах будет почти бесполезно. Впрочем, .h тоже в скомпилированном виде не распространишь.

firkax ★★★★★
()
Последнее исправление: firkax (всего исправлений: 2)
Ответ на: комментарий от SaBo

https://en.cppreference.com/w/cpp/language/templates

The definition of a template must be visible at the point of implicit instantiation, which is why template libraries typically provide all template definitions in the headers

у тебя объявление и описание Stream в разных файлах, так работать не будет, как минимум при таком включении файлов

xgatron
()

Как @xgatron написал объедини файлы stream.h и stream.cpp (чтобы всё в h было) и тогда заработает.

Можешь ещё посмотреть ассеблерный код этих двух примеров: https://gcc.godbolt.org/z/Wer9KKazx

fsb4000 ★★★★★
()
Последнее исправление: fsb4000 (всего исправлений: 2)

Вандевурд, Дэвид, Джосаттис, Николаи М., Грегор, Дуглас. Шаблоны C++. Справочник разработчика, 2-е изд Глава 9.1.1:

Компилятор C++, вероятно, примет эту программу без каких-либо проблем, но компоновщик, скорее всего, сообщит об ошибке отсутствия определения функции printTypeof(). Причиной этой ошибки является то, что определение шаблона функции printTypeof() не инстанцировано. Чтобы шаблон был инстанцирован, компи­лятор должен знать, какие определения должны быть инстанцированы и для ка­ких именно аргументов шаблона. К сожалению, в предыдущем примере эти две части информации находятся в файлах, компилируемых по отдельности. Таким образом, когда наш компилятор видит вызов printTypeof (), но не имеет опре­деления шаблона, чтобы инстанцировать его для double, он просто предполага­ет, что такое определение представлено в другом месте, и создает для этого опре­деления соответствующую ссылку (разрешаемую компоновщиком). С другой стороны, когда компилятор обрабатывает файл myfirst.срр, он не имеет ника­ких указаний о том, что он должен инстанцировать определение содержащегося в нем шаблона для некоторых конкретных аргументов.

Далее Глава 9.1.2 9.1.2. Шаблоны в заголовочных файлах Обычное решение описанной проблемы заключается в использовании того же подхода, что и для макросов или встраиваемых функций: мы включаем определе­ния шаблона в заголовочный файл, объявляющий этот шаблон.

rumgot ★★★★★
()
Последнее исправление: rumgot (всего исправлений: 2)

Благодарю за ответы! Не скажу что стало совсем понятно, но стало понятно что нужно изучить чтобы понять как на самом деле работают шаблоны.

SaBo ★★
() автор топика

Все уже сказали почему не работает, не сказали как надо делать что бы работало. Есть два варианта:

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

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

template Stream<int>;

подробнее см. у Вандервуда

AntonI ★★★★
()
Ответ на: комментарий от firkax

Библиотека тут ни при чём

Господа, а я таки посмотрел повнимательней что ТС делает, и походу я всех сбил с толку - у него template из библиотечки не торчит…

@SaBo: Вы хотите переименовать Stream.cpp в Stream.hpp, и заинклудить в конце Stream.h. И будет вам счастье ;)

bugfixer ★★★★
()
Ответ на: комментарий от bugfixer

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

firkax ★★★★★
()
Последнее исправление: firkax (всего исправлений: 1)
Ответ на: комментарий от firkax

Неинстанцированный шаблон там полюбому есть.

Всё правильно. Просто я сначала подумал что этот шаблон является частью public interface библиотечки, а это всего лишь implementation detail. Другими словами - конечно .so должен быть self-contained, и клиенты библиотечки не должны заботится что там и как там внутри инстанциируется. Рецепт «что с этим делать» правда от этого сильно не меняется.

bugfixer ★★★★
()

у тебя класс Stream «int» нигде не инстанцируется целиком.

#include "lib.h"
#include "stream.h"

Lib::Lib() {}

void Lib::test()
{
    Stream<int> s;
    s.test();
}

тут не инстанцируется, поскольку в хидере шаблона нет тел методов, а только декларации. инстанциацию делает компилятор, видя код тел методов шаблона класса. если он тела не видит, а только заголовки, как у тебя - ему кажется, что шаблон инстанцируется как-то иначе. потому либо ты должен шаблон инстанцировать ручками в stream.cpp, либо вытащить тела методов шаблона в stream.h

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

alysnix ★★★
()
Последнее исправление: alysnix (всего исправлений: 1)
Ответ на: комментарий от alysnix

Тогда почему при сборке либы не было ошибки линковки?

Ведь в либе есть Stream<int> s;
т.е. должно было вызваться инстанцирование?

Почему не было ошибки при конпеляции lib.cpp - понятно.
Но при сборке конечной либы - линкер же должен быть увидеть что никто не содержит инстанциации Stream<int>.

vollemar
()
Ответ на: комментарий от vollemar

Ведь в либе есть Stream s; т.е. должно было вызваться инстанцирование?

с какой стати будет инстанцирование??? компилятор видит только шаблон с заголовками, все что он может тут сделать - «инстанцировать»(то есть правильно типизировать) заголовок класса, и ему этого достаточно. тел то он не видит. а больше инстанцировать(то есть скомпилировать код методов с данными параметрами шаблона) некому.

Но при сборке конечной либы - линкер же должен быть увидеть что никто не содержит инстанциации Stream…

это хитрый вопрос. скорее всего линкеру тут наплевать. он думает, что когда либа будет ликоваться ее потребителем, там этот символ будет. например придет из другой либы.

alysnix ★★★
()
Последнее исправление: alysnix (всего исправлений: 1)
Ответ на: комментарий от vollemar

Но при сборке конечной либы - линкер же должен быть увидеть что никто не содержит инстанциации Stream.

Потуглите «gcc no-undefined», выпадает в частности это и вот это. Думаю это именно то что Вы ищете.

Возможно примерчик ниже Вам немного прояснит что происходит:

$ cat mylib.cpp 
#include <iostream>

template<class T> struct Foo {
   static void Bar(T val);
};

#ifndef HIDEME
template<class T>
void Foo<T>::Bar(T val) {
   using namespace std;
   cout << "val: " << val << endl;
}
#endif

void mylibFunc()
{
   Foo<int>::Bar(1);
}

$ g++ -fPIC -shared -g -o mylib.so mylib.cpp 
$ nm -o mylib.so | c++filt | fgrep Foo
mylib.so:00000000000011c4 W Foo<int>::Bar(int)

$ g++ -fPIC -shared -g -o mylib.so -DHIDEME mylib.cpp 
$ nm -o mylib.so | c++filt | fgrep Foo
mylib.so:                 U Foo<int>::Bar(int)

$ g++ -fPIC -shared -g -o mylib.so -DHIDEME -Wl,--no-undefined mylib.cpp 
/usr/bin/ld: /tmp/ccWBgitV.o: in function `mylibFunc()':
mylib.cpp:17: undefined reference to `Foo<int>::Bar(int)'
collect2: error: ld returned 1 exit status

$ g++ -fPIC -shared -g -o mylib.so -Wl,--no-undefined mylib.cpp 
$ nm -o mylib.so | c++filt | fgrep Foo
mylib.so:00000000000011c4 W Foo<int>::Bar(int)
bugfixer ★★★★
()
Последнее исправление: bugfixer (всего исправлений: 4)
Ответ на: комментарий от bugfixer

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

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

alysnix ★★★
()
Ответ на: комментарий от alysnix

библиотека не обязана содержать все используемые символы внутри нее

Правда. Но какое это имеет отношение к «-Wl,-no-undefined»? Подозреваю что Вы не до конца понимаете что именно оно делает:

$ nm -o mylib.so | c++filt | fgrep 'U '
mylib.so:                 U __cxa_atexit@@GLIBC_2.2.5
mylib.so:                 U std::basic_ostream<char, std::char_traits<char> >::operator<<(int)@@GLIBCXX_3.4
mylib.so:                 U std::basic_ostream<char, std::char_traits<char> >::operator<<(std::basic_ostream<char, std::char_traits<char> >& (*)(std::basic_ostream<char, std::char_traits<char> >&))@@GLIBCXX_3.4
mylib.so:                 U std::ios_base::Init::Init()@@GLIBCXX_3.4
mylib.so:                 U std::ios_base::Init::~Init()@@GLIBCXX_3.4
mylib.so:                 U std::cout@@GLIBCXX_3.4
mylib.so:                 U std::basic_ostream<char, std::char_traits<char> >& std::endl<char, std::char_traits<char> >(std::basic_ostream<char, std::char_traits<char> >&)@@GLIBCXX_3.4
mylib.so:                 U std::basic_ostream<char, std::char_traits<char> >& std::operator<< <std::char_traits<char> >(std::basic_ostream<char, std::char_traits<char> >&, char const*)@@GLIBCXX_3.4

у топикстартера нет явной инстанциации, и всем кажется, что она где-то в другом месте

Очевидно что у ТС шаблончик - «private implementation detail of a lib». Ну вот не будет его инстанциаций «в другом месте», по определению.

bugfixer ★★★★
()
Ответ на: комментарий от bugfixer

я не хочу разбираться в ваших экспериментах и простынях.

суть проблемы я показал(темплейт не инстанцируется), борьба с ней может быть разными способами - тем или иным способом инстанцируй его.

вопрос надо закрывать.

alysnix ★★★
()
Ответ на: комментарий от alysnix

я не хочу разбираться в ваших экспериментах

Напрасно. Я всё больше убеждаюсь в том что Ваши познания весьма поверхностны.

и простынях.

Простыней не было. Был bare min чтобы продемонстрировать point.

вопрос надо закрывать.

Вот хоть бы раз сознались что «я протогойто».

bugfixer ★★★★
()
Ответ на: комментарий от alysnix

Не удержался.

я не хочу разбираться в ваших экспериментах и простынях.

Вы даже распарсить не смогли что именно я сказал.

суть проблемы я показал

Обсосали со всех сторон уже задолго до того как «ваше величество» соизволило уделить минутку внимания «этому чатику».

вопрос надо закрывать.

Так точно. Вы закончили?

bugfixer ★★★★
()
Ответ на: комментарий от vollemar

Тогда почему при сборке либы не было ошибки линковки?

Тела методов инстацируются по мере необходимости. В указанном примере необходимы были тока конструктор и деструктор.

И нет, линкер не проверяет что в либе есть все, такое всплывает только при сборке исполняемого файла или вообще при запуске.

AntonI ★★★★
()

Потому что у тебя нет класса, а есть шаблон. Т.е. описание на встроенном языке как классы типа stream<что-то> создавать. В одном модуле компиляции (stream.cpp) написано как такие файлы создавать, но не написано какие нужны. В другом (lib.cpp) известно что классы stream<*> бывают (из stream.h) и что где-то существует реализация stream<int>. И только линковщик замечает что по факту stream<int> нигде нет. Когда-то, в 90х, у Страуструпа была идея, что ты в своем stream.cpp мог бы написать export template <...> class stream... и компилятор бы понял, что в stream.o нужно включить информацию о шаблоне и уже во время линковки линковщик вызвал бы компилятор и нагенерировал все недостающие варианты stream<*>. Но это вроде так нигде и не реализовали и под конец, вроде, так и выкинули.

khrundel ★★★★
()
Ответ на: комментарий от interrupted

Как тебе уже выше написали — обмазываться шаблонами совершенно необязательно (кроме типов из STL), можно использовать C++ как «С с классами». Шаблоны — это мощное средство для случаев, например, когда тебе надо одни и те же алгоритмы применять для разных типов данных. Несколько утрируя, можно рассматривать шаблоны как «умные макросы».

hobbit ★★★★★
()
Ответ на: комментарий от hobbit

Меня немножко смутило другое. ТС и (я так подозреваю) его знакомый были озадачены вопросом - «как же так - so’шка линкуется и интерфейс библиотечки от параметров внутреннего шаблона не зависит» (в какой-то момент я это не совсем правильно сформулировал, конкретно вот здесь), но тем не менее при линковке final executable таки вылезают ошибки. Честно скажу - практического опыта деплоя .so у меня минимум (в силу многих причин мы не можем себе позволить stable library API, и поэтому наши либы всегда линкуются статически). Меня это волновало исключительно к точки зрения «ну не может это быть какой-то нестандартной ситуацией - миллион «леммингов» давно уже должен был с этим столкнуться и найти решение». И таки оно есть! По крайней мере с GNU toolchain. Можно «попросить» линкер проверить отсутствие unresolved symbols в .so (учитывая все implicit and explicit deps). Неужели для меня одного это было открытием? Тогда «снимаю шляпу» перед присутствующими…

bugfixer ★★★★
()
Ответ на: комментарий от bugfixer

Ну вообще-то, в плюсах всегда было так и не только с шаблонами. Если в .so чего-то нет, предполагается, что оно должно быть где-то ещё.

Одна из самых весёлых ситуаций с Qt, например - это когда мы подгружаем драйвер некоей СУБД и получаем сообщение, что загрузка не удалась. Причём какая именно библиотека не загрузилась, по сообщению понять невозможно. К примеру, нету libiconv, которую тянет libpq, которую в свою очередь тянет драйвер QPSQL — исход один, «driver not loaded».

Причём в рантайме это поправить, как я понимаю, невозможно, поскольку используются функции динамической загрузки (dlopen() в юниксах и LoadLibrary() в Windows), а у них куцая диагностика по определению.

hobbit ★★★★★
()
Последнее исправление: hobbit (всего исправлений: 2)
16 декабря 2022 г.

нечитая ответов скажу, что в случае с шаблонами, вынесение реализации в отдельный файл геморное занятие. Получается, что у тебя объектник по stream.cpp пустой вообще. Шаблоны не являются типами. Они могут «превратиться» в тип — инстанциироваться. Существует «автоматическая» и «ручная» инстанциация — первая происходит если использовать в запускаемом коде шаблон с указанием его аргументов, вторая происходит если вручную сделать инстанциацию подобно описанию специализации шаблона (на самом деле специализация шаблона так же и производит ручную инстанциацию).
В stream.cpp у тебя нет ручной инстанциации, по этому и пустым получается объектник — выход либо использовать хедер-онли либу stream.h, либо делать ручную инстанциацию в stream.cpp для определенных типов, что неудобно и неуниверсально (придется для каждого используемого в аргументах шаблонов значений изменять код в stream.cpp, что анулирует многие преимущества шаблонов).

safocl ★★
()
Последнее исправление: safocl (всего исправлений: 1)
Ответ на: комментарий от SaBo

в стандарте с++ сказано, что каждая единица трансляции компилируется отдельно — это означает буквально, что они не подозревают про существование других. И при компиляции объектного файла из stream.cpp компилятор не знает с какими значениями аргументов шаблонов он применялся в других единицах трансляции по данной причине невозможно для него предугадать без явной инстанциации, с для каких значений аргументов шаблонов инстанциировать сей шаблон.

safocl ★★
()
Последнее исправление: safocl (всего исправлений: 1)