1 Востаннє редагувалося leofun01 (01.08.2017 01:54:45)

Тема: Чому не можна розбивати шаблони на h і cpp файли ? [Стаття застаріла]

Зазвичай, коли ми пишемо код на С++, ми розділюємо його на h і cpp файли, в h файли пишемо оголошення функцій/класів, а в cpp файли - їх реалізацію.

Приклад

оголошення

// functions.h

#pragma once

void function(int value);

реалізація

// functions.cpp

#include "functions.h"

void function(int value) {
    // Some code
}

використання

// main.cpp

#include "functions.h"

void main() {
    function(1);
}

І все прекрасно компілюється і запускається.
Але коли ми робимо так з шаблонами (template<...>):

оголошення

// functions.h

#pragma once

template<typename T>
void function(T value);

реалізація

// functions.cpp

#include "functions.h"

template<typename T>
void function(T value) {
    // Some code
}

використання

// main.cpp

#include "functions.h"

void main() {
    function<int>(1);
}

то середовище розробки видає помилки компоновщика :( :

VisualStudio написав:

error LNK2019: unresolved external symbol "void __cdecl function<int>(int)" (??$function@H@@YAXH@Z) in function _main
error LNK1120: 1 unresolved externals

error LNK2019: ссылка на неразрешенный внешний символ "void __cdecl function<int>(int)" (??$function@H@@YAXH@Z) в функции _main
error LNK1120: 1 неразрешенных внешних элементов

Не надто інформативні повідомлення, правда ?

Для того щоб розібратися, в чому проблема, потрібно знати що таке шаблон (template) і як працюють компілятор (compiler) і компоновщик (linker).

Шаблони:
Шаблони функцій - це частини коду, які починаються з "template<typename T>" (новий стиль) або "template<class T>" (старий стиль), далі виглядають як звичайні функції (за винятком типу T) і описують однакову поведінку для різних типів-параметрів T. Замість T можуть бути будь-які інші імена. При цьому вони не є реалізаціями функцій, лише заготовками. Це одна з причин, чому не варто їх класти в cpp файли. Також вони не є повноцінними оголошеннями.
Реалізації (і/або оголошення) функцій формуються тоді, коли щось їх викликає, передаючи відомі типи в якості типу-параметра.
Шаблони класів і структур точно такі самі, тільки у відношенні класів і структур, а не функцій.
Висновок: Шаблони не можуть оголошувати і реалізувати щось, можуть тільки описувати оголошення і реалізації. Ці описи оголошень і реалізацій перетворюються в самі оголошення і реалізації, коли їх (шаблони, описи) викликають, передаючи відомі типи як параметри шаблону.

Компіляція і компонування:
Спочатку компілятор бере всі файли, які підлягають компіляції (зазвичай: h, c і cpp файли), перевіряє їх на наявність помилок, і якщо помилок немає, формує відповідні obj файли і передає їх компоновщику, або повертає їх в середовище розробки і воно передає їх компоновщику.
Тобто, якщо в нас є файли "functions.h", "functions.cpp" і "main.cpp", то будемо мати "functions.obj" і "main.obj".
Далі компоновщик перевіряє obj файли на наявність помилок, і якщо їх немає, формує dll або exe файл.

Приблизно як на малюнках

https://i.ibb.co/rkFgDkG/Compiler01.png
https://i.ibb.co/HKYjBXp/Linker01.png
https://i.ibb.co/ydvSvCV/dll-exe-01.png

Якщо хоча б в одному obj файлі є виклик функцій/класів, які не реалізовані в жодному з obj файлів, то це помилка, виконуваний файл не зформується і компоновщик поверне помилки.
Саме такого типу помилка сталася в наведеному прикладі.
Але чому ?

По порядку:
Синтаксичних помилок немає, тому компілятор видав нам 2 файли "functions.obj" і "main.obj".
Компоновщик заглянув в "main.obj" і зустрів там виклик функції "function<int>(1);", яка не реалізована в самому "main.obj" (хоч і оголошена в ньому), тому він шукає реалізацію в інших файлах, в нашому випадку тільки в "functions.obj". І "functions.obj" майже порожній, в ньому також немає реалізації цієї функції.
https://pic.co.ua/images/2016/03/19/677a9467d48abd2119c90934d4bfead1.png
Шаблони не можуть оголошувати і реалізувати ...
Але "main.obj" містить оголошення цієї функції. Як це розуміти ?
Описи оголошень і реалізацій перетворюються в самі оголошення і реалізації, коли їх викликають ...
Тобто, коли компілятор побачив "function<int>(1);" в "main.cpp", він підтягнув оголошення з файлу "functions.h", бо в "main.cpp" є #include"functions.h", перетворив його на:

void function(int value);

і вставив в "main.obj", а реалізацію не вставив, бо "functions.cpp" не був підключений в "main.cpp".
От і маємо один із способів вирішити проблему, підключити "functions.cpp" в "main.cpp":

// main.cpp

#include "functions.h"
#include "functions.cpp"

void main() {
    function<int>(1);
}

Але це трохи не гарно.
Краще вирізати шаблонний код з "functions.cpp" і вставити в "functions.h". Де саме вставляти код ? Найкраще в кінець файлу (за умови, що код не спричиняє інших помилок):

// functions.h

#pragma once

template<typename T>
void function(T value);

template<typename T>
void function(T value) {
    // Some code
}
// main.cpp

#include "functions.h"

void main() {
    function<int>(1);
}

А файл "functions.cpp" можна видалити, якщо в ньому були тільки шаблони.

https://i.imgflip.com/k69be.jpg

2 Востаннє редагувалося leofun01 (20.03.2016 13:28:01)

Re: Чому не можна розбивати шаблони на h і cpp файли ? [Стаття застаріла]

Думка про те, що шаблони не можна розбивати, не давала мені спокою, і я шукав способи, як впарити компілятору розбиті шаблони так, щоб він записував в obj файл реалізації.
І здається знайшов спосіб:

h

// functions.h

#ifndef FUNCTIONS_H
#define FUNCTIONS_H

template<typename T>
void function(T value);

#include"functions.cpp"
#endif

cpp

// functions.cpp

#include"functions.h"

#ifndef FUNCTIONS_CPP
#define FUNCTIONS_CPP

template<typename T>
void function(T value) {
    // Some code
}

#endif

main

// main.cpp

#include"functions.h"

void main() {
    function<int>(1);
}

Не знаю, чи працює це на інших компіляторах, але VisualStudio схавала цей код нормально.

3

Re: Чому не можна розбивати шаблони на h і cpp файли ? [Стаття застаріла]

Звісно схаває, але include ".cpp" мені тхне.
Просто треба розуміти, що шаблони, хоча й схожі на функції, є насправді контрольованими компілятором макросами. В .cpp треба виносите те, що утворює об'єктний код - а шаблони з очевидних причин його не утворюють. От і все.

4

Re: Чому не можна розбивати шаблони на h і cpp файли ? [Стаття застаріла]

Це я просто як варіант виставив, так би мовити, для справжніх збоченців :D .

Подякували: Дмитро-Чебурашка1

5

Re: Чому не можна розбивати шаблони на h і cpp файли ? [Стаття застаріла]

Для справжніх збоченців:

/**
 * foo.hpp
 */
#pragma once

template <typename T>
void foo(T value);
/**
 * foo.cpp
 */
#include "foo.hpp"
#include <iostream>

template <typename T>
void foo(T value) {
    std::cout << value << std::endl;
}

template void foo<int>(int);
/**
 * main.cpp
 */
#include "foo.hpp"

int main() {
    foo(123);
}
Program returned: 0
Program stdout
123
Подякували: leofun011