Явная реализация - когда это используется?

После нескольких недель перерыва я пытаюсь расширить и расширить свои знания о шаблонах с помощью книги " Шаблоны - Полное руководство " Дэвида Вандевурда и Николая М. Йосуттиса, и в данный момент я пытаюсь понять, что такое явная реализация шаблонов.,

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

3 ответа

Решение

Непосредственно скопировано с https://docs.microsoft.com/en-us/cpp/cpp/explicit-instantiation:

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

(Например, libstdC++ содержит явную реализацию std::basic_string<char,char_traits<char>,allocator<char> > (который std::string) так что каждый раз, когда вы используете функции std::stringодин и тот же код функции не нужно копировать в объекты. Компилятору нужно только ссылаться (связывать) их с libstdC++.)

Если вы определите шаблонный класс, который вы хотите использовать только для нескольких явных типов.

Поместите объявление шаблона в заголовочный файл, как обычный класс.

Поместите определение шаблона в исходный файл, как обычный класс.

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

Глупый пример:

// StringAdapter.h
template<typename T>
class StringAdapter
{
     public:
         StringAdapter(T* data);
         void doAdapterStuff();
     private:
         std::basic_string<T> m_data;
};
typedef StringAdapter<char>    StrAdapter;
typedef StringAdapter<wchar_t> WStrAdapter;

Источник:

// StringAdapter.cpp
#include "StringAdapter.h"

template<typename T>
StringAdapter<T>::StringAdapter(T* data)
    :m_data(data)
{}

template<typename T>
void StringAdapter<T>::doAdapterStuff()
{
    /* Manipulate a string */
}

// Explicitly instantiate only the classes you want to be defined.
// In this case I only want the template to work with characters but
// I want to support both char and wchar_t with the same code.
template class StringAdapter<char>;
template class StringAdapter<wchar_t>;

Главный

#include "StringAdapter.h"

// Note: Main can not see the definition of the template from here (just the declaration)
//       So it relies on the explicit instantiation to make sure it links.
int main()
{
  StrAdapter  x("hi There");
  x.doAdapterStuff();
}

Явное создание экземпляров позволяет сократить время компиляции и размеры объектов.

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

  • удалите определения из заголовков, чтобы инструменты сборки не перестраивали включающие элементы
  • переопределение объекта

Удалить определения из заголовков

Явное создание экземпляров позволяет оставлять определения в файле.cpp.

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

Помещение определений в файлы.cpp имеет обратную сторону: внешние библиотеки не могут повторно использовать шаблон со своими собственными новыми классами, но "Удалить определения из включенных заголовков, но также предоставить шаблоны внешнего API" ниже показывает обходной путь.

См. Конкретные примеры ниже.

Преимущества переопределения объекта: понимание проблемы

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

Это означает много бесполезного использования диска и время компиляции.

Вот конкретный пример, в котором оба main.cpp а также notmain.cpp неявно определять MyTemplate<int> из-за его использования в этих файлах.

main.cpp

#include <iostream>

#include "mytemplate.hpp"
#include "notmain.hpp"

int main() {
    std::cout << notmain() + MyTemplate<int>().f(1) << std::endl;
}

notmain.cpp

#include "mytemplate.hpp"
#include "notmain.hpp"

int notmain() { return MyTemplate<int>().f(1); }

mytemplate.hpp

#ifndef MYTEMPLATE_HPP
#define MYTEMPLATE_HPP

template<class T>
struct MyTemplate {
    T f(T t) { return t + 1; }
};

#endif

notmain.hpp

#ifndef NOTMAIN_HPP
#define NOTMAIN_HPP

int notmain();

#endif

GitHub вверх по течению.

Компилировать и просматривать символы с помощью nm:

g++ -c -Wall -Wextra -std=c++11 -pedantic-errors -o notmain.o notmain.cpp
g++ -c -Wall -Wextra -std=c++11 -pedantic-errors -o main.o main.cpp
g++    -Wall -Wextra -std=c++11 -pedantic-errors -o main.out notmain.o main.o
echo notmain.o
nm -C -S notmain.o | grep MyTemplate
echo main.o
nm -C -S main.o | grep MyTemplate

Выход:

notmain.o
0000000000000000 0000000000000017 W MyTemplate<int>::f(int)
main.o
0000000000000000 0000000000000017 W MyTemplate<int>::f(int)

От man nm, Мы видим, что Wозначает слабый символ, который выбрал GCC, потому что это шаблонная функция. Слабый символ означает, что скомпилированный неявно сгенерированный код дляMyTemplate<int> был скомпилирован для обоих файлов.

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

Цифры в выходных данных означают:

  • 0000000000000000: адрес в разделе. Этот ноль объясняется тем, что шаблоны автоматически помещаются в отдельный раздел.
  • 0000000000000017: размер сгенерированного для них кода

Мы можем увидеть это немного яснее:

objdump -S main.o | c++filt

который заканчивается на:

Disassembly of section .text._ZN10MyTemplateIiE1fEi:

0000000000000000 <MyTemplate<int>::f(int)>:
   0:   f3 0f 1e fa             endbr64 
   4:   55                      push   %rbp
   5:   48 89 e5                mov    %rsp,%rbp
   8:   48 89 7d f8             mov    %rdi,-0x8(%rbp)
   c:   89 75 f4                mov    %esi,-0xc(%rbp)
   f:   8b 45 f4                mov    -0xc(%rbp),%eax
  12:   83 c0 01                add    $0x1,%eax
  15:   5d                      pop    %rbp
  16:   c3                      retq

а также _ZN10MyTemplateIiE1fEi это искаженное имя MyTemplate<int>::f(int)> который c++filt решил не распутывать.

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

Решения проблемы переопределения объекта

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

  • перемещая определение в файл cpp, оставьте только объявление в hpp, т.е. измените исходный пример следующим образом:

    mytemplate.hpp

    #ifndef MYTEMPLATE_HPP
    #define MYTEMPLATE_HPP
    
    template<class T>
    struct MyTemplate {
        T f(T t);
    };
    
    #endif
    

    mytemplate.cpp

    #include "mytemplate.hpp"
    
    template<class T>
    T MyTemplate<T>::f(T t) { return t + 1; }
    
    // Explicit instantiation.
    template class MyTemplate<int>;
    

    Оборотная сторона: внешние проекты не могут использовать ваш шаблон со своими типами. Также вы вынуждены явно создавать экземпляры всех типов. Но, возможно, это положительный момент, поскольку тогда программисты не забудут.

  • сохранить определение на hpp и добавить extern templateдля каждого включающего устройства см. также: использование шаблона extern (C++11), т.е. измените исходный пример следующим образом:

    mytemplate.cpp

    #include "mytemplate.hpp"
    
    // Explicit instantiation.
    template class MyTemplate<int>;
    

    main.cpp

    #include <iostream>
    
    #include "mytemplate.hpp"
    #include "notmain.hpp"
    
    // extern template declaration
    extern template class MyTemplate<int>;
    
    int main() {
        std::cout << notmain() + MyTemplate<int>().f(1) << std::endl;
    }
    

    notmain.cpp

    #include "mytemplate.hpp"
    #include "notmain.hpp"
    
    // extern template declaration
    extern template class MyTemplate<int>;
    
    int notmain() { return MyTemplate<int>().f(1); }
    

    Оборотная сторона: все участники должны добавить extern в свои файлы CPP, что программисты, скорее всего, забудут сделать.

  • сохранить определение на hpp и добавить extern template на hpp для типов, экземпляры которых будут явно созданы:

    mytemplate.hpp

    #ifndef MYTEMPLATE_HPP
    #define MYTEMPLATE_HPP
    
    template<class T>
    struct MyTemplate {
        T f(T t) { return t + 1; }
    };
    
    extern template class MyTemplate<int>;
    
    #endif
    

    mytemplate.cpp

    #include "mytemplate.hpp"
    
    // Explicit instantiation required just for int.
    template class MyTemplate<int>;
    

    main.cpp

    #include <iostream>
    
    #include "mytemplate.hpp"
    #include "notmain.hpp"
    
    int main() {
        std::cout << notmain() + MyTemplate<int>().f(1) << std::endl;
    }
    

    notmain.cpp

    #include "mytemplate.hpp"
    #include "notmain.hpp"
    
    int notmain() { return MyTemplate<int>().f(1); }
    

    Обратная сторона: вы заставляете внешние проекты выполнять собственное явное создание экземпляров.

С любым из этих решений nm теперь содержит:

notmain.o
                 U MyTemplate<int>::f(int)
main.o
                 U MyTemplate<int>::f(int)
mytemplate.o
0000000000000000 W MyTemplate<int>::f(int)

так что мы видим только mytemplate.o есть сборник MyTemplate<int> по желанию, а notmain.o а также main.o не потому что U означает undefined.

Удалите определения из включенных заголовков, но также предоставьте шаблоны внешнего API

Наконец, есть еще один вариант использования, который следует рассмотреть, если вы хотите и то, и другое:

  • ускорить компиляцию вашего проекта
  • выставлять заголовки как API внешней библиотеки, чтобы другие могли его использовать

Чтобы решить эту проблему, вы можете выполнить одно из следующих действий:

    • mytemplate.hpp: определение шаблона
    • mytemplate_interface.hpp: объявление шаблона соответствует только определениям из mytemplate_interface.hpp, без определений
    • mytemplate.cpp: включают mytemplate.hpp и сделать явные мгновенные
    • main.cpp и везде в базе кода: include mytemplate_interface.hppне mytemplate.hpp
    • mytemplate.hpp: определение шаблона
    • mytemplate_implementation.hpp: включает mytemplate.hpp и добавляет extern для каждого класса, который будет создан
    • mytemplate.cpp: включают mytemplate.hpp и сделать явные мгновенные
    • main.cpp и везде в базе кода: include mytemplate_implementation.hppне mytemplate.hpp

Или, возможно, еще лучше для нескольких заголовков: создайте intf/impl папка внутри вашего includes/ папка и используйте mytemplate.hpp как имя всегда.

В mytemplate_interface.hpp подход выглядит так:

mytemplate.hpp

#ifndef MYTEMPLATE_HPP
#define MYTEMPLATE_HPP

#include "mytemplate_interface.hpp"

template<class T>
T MyTemplate<T>::f(T t) { return t + 1; }

#endif

mytemplate_interface.hpp

#ifndef MYTEMPLATE_INTERFACE_HPP
#define MYTEMPLATE_INTERFACE_HPP

template<class T>
struct MyTemplate {
    T f(T t);
};

#endif

mytemplate.cpp

#include "mytemplate.hpp"

// Explicit instantiation.
template class MyTemplate<int>;

main.cpp

#include <iostream>

#include "mytemplate_interface.hpp"

int main() {
    std::cout << MyTemplate<int>().f(1) << std::endl;
}

Скомпилируйте и запустите:

g++ -c -Wall -Wextra -std=c++11 -pedantic-errors -o mytemplate.o mytemplate.cpp
g++ -c -Wall -Wextra -std=c++11 -pedantic-errors -o main.o main.cpp
g++    -Wall -Wextra -std=c++11 -pedantic-errors -o main.out main.o mytemplate.o

Выход:

2

Протестировано в Ubuntu 18.04.

Модули C++20

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

Я думаю, что эта функция обеспечит наилучшую настройку в будущем, когда она станет доступной, но я еще не проверял ее, потому что она еще не доступна в моем GCC 9.2.1.

Вам все равно придется делать явную инстанциацию, чтобы получить ускорение / экономию на диске, но, по крайней мере, у нас будет разумное решение для "Удалять определения из включенных заголовков, но также предоставлять шаблоны внешнего API", которое не требует копирования чего-либо примерно 100 раз.

Ожидаемое использование (без явного озарения, не уверен, какой будет точный синтаксис, см.: Как использовать явное создание экземпляра шаблона с модулями C++20?) Должно быть что-то вроде:

helloworld.cpp

export module helloworld;  // module declaration
import <iostream>;         // import declaration
 
template<class T>
export void hello(T t) {      // export declaration
    std::cout << t << std::end;
}

main.cpp

import helloworld;  // import declaration
 
int main() {
    hello(1);
    hello("world");
}

а затем компиляция, указанная на https://quuxplusone.github.io/blog/2019/11/07/modular-hello-world/

clang++ -std=c++2a -c helloworld.cpp -Xclang -emit-module-interface -o helloworld.pcm
clang++ -std=c++2a -c -o helloworld.o helloworld.cpp
clang++ -std=c++2a -fprebuilt-module-path=. -o main.out main.cpp helloworld.o

Итак, из этого мы видим, что clang может извлечь интерфейс шаблона + реализацию в волшебный helloworld.pcm, который должен содержать некоторое промежуточное представление источника LLVM: Как шаблоны обрабатываются в модульной системе C++? что по-прежнему допускает возможность спецификации шаблона.

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

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

Приведенный ниже анализ может помочь вам решить или, по крайней мере, выбрать наиболее многообещающие объекты для рефакторинга в первую очередь во время экспериментов, заимствуя некоторые идеи из: Мой объектный файл C++ слишком велик

# List all weak symbols with size only, no address.
find . -name '*.o' | xargs -I{} nm -C --size-sort --radix d '{}' |
  grep ' W ' > nm.log

# Sort by symbol size.
sort -k1 -n nm.log -o nm.sort.log

# Get a repetition count.
uniq -c nm.sort.log > nm.uniq.log

# Find the most repeated/largest objects.
sort -k1,2 -n nm.uniq.log -o nm.uniq.sort.log

# Find the objects that would give you the most gain after refactor.
# This gain is calculated as "(n_occurences - 1) * size" which is
# the size you would gain for keeping just a single instance.
# If you are going to refactor anything, you should start with the ones
# at the bottom of this list. 
awk '{gain = ($1 - 1) * $2; print gain, $0}' nm.uniq.sort.log |
  sort -k1 -n > nm.gains.log

# Total gain if you refactored everything.
awk 'START{sum=0}{sum += $1}END{print sum}' nm.gains.log

# Total size. The closer total gain above is to total size, the more
# you would gain from the refactor.
awk 'START{sum=0}{sum += $1}END{print sum}' nm.log

Мечта: кеш компилятора шаблонов

Я думаю, что окончательным решением было бы, если бы мы могли построить с:

g++ --template-cache myfile.o file1.cpp
g++ --template-cache myfile.o file2.cpp

а потом myfile.o автоматически повторно использует ранее скомпилированные шаблоны в файлах.

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

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

Страница GNU C++ обсуждает модели здесь https://gcc.gnu.org/onlinedocs/gcc-4.5.2/gcc/Template-Instantiation.html.

Другие вопросы по тегам