Автоматизация явного создания экземпляра шаблона

Чтобы сократить время компиляции в проекте с большим количеством шаблонов, я пытаюсь явно создавать экземпляры многих шаблонов в отдельной единице компиляции. Поскольку эти шаблоны зависят от enum class элементов, я могу перечислить все возможные экземпляры. Я хочу, чтобы все остальные cpp-файлы видели только объявление. Хотя я могу это сделать, я сталкиваюсь с проблемами, пытаясь разложить на множители явные экземпляры. Сначала я объясню 2 рабочих примера ниже, чтобы объяснить, в чем именно заключается моя проблема (пример 3):

Пример 1

/* test.h
   Contains the function-template-declaration, not the implementation.
*/

enum class Enum
{
    Member1,
    Member2,
    Member3
};

struct Type
{
    template <Enum Value>
    int get() const;
};
/* test.cpp
   Only the declaration is visible -> needs to link against correct instantiation.
*/

#include "test.h"

int main() {
    std::cout << Type{}.get<Enum::Member1>() << '\n';
}
/* test.tpp 
   .tpp extension indicates that it contains template implementations.
*/

#include "test.h"

template <Enum Value>
int Type::get() const
{
    return static_cast<int>(Value); // silly implementation
}
/* instantiate.cpp
   Explicitly instantiate for each of the enum members.
*/

#include "test.tpp"

template int Type::get<Enum::Member1>() const;
template int Type::get<Enum::Member2>() const;
template int Type::get<Enum::Member3>() const;

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

Пример 2

// instantiate.cpp

#include "test.tpp"

template <Enum Value>
struct Instantiate
{
    using Function = int (Type::*)() const;
    static constexpr Function f1 = Type::get<Value>;
   
    // many more member-functions
};

template class Instantiate<Enum::Member1>;
template class Instantiate<Enum::Member2>;
template class Instantiate<Enum::Member3>;

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

Пример 3

// instantiate.cpp

#include "test.tpp"

template <Enum Value>
struct Instantiate { /* same as before */ };

template <Enum ... Pack>
struct InstantiateAll:
    Instantiate<Pack> ...
{};

template class InstantiateAll<Enum::Member1, Enum::Member2, Enum::Member3>; 

Это должно сработать, верно? Чтобы создать экземпляр InstantiateAll<...>, необходимо создать экземпляр каждого из производных классов. По крайней мере, я так думал. Вышеприведенное компилируется, но приводит к ошибке компоновщика. После проверки таблицы символов instantiate.o с помощью nm было подтверждено, что вообще ничего не было создано. Почему нет?

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

(Компиляция с помощью GCC 10.2.0)

Изменить: то же самое происходит в Clang 8.0.1 (хотя я должен явно использовать адрес оператора при назначении указателей функций: Function f1 = &Type::get<Value>;)

Изменить: пользователь 2b-t любезно предоставил доступ к примерам через https://www.onlinegdb.com/HyGr7w0fv_ для экспериментов с людьми.


comment
Не по теме: вы уверены, что вам нужны файлы tpp? Я имею в виду: как насчет определения (struct Type { template <Enum Value> int get () const { return static_cast<int>(Value); } };) методов непосредственно внутри test.h?   -  person max66    schedule 25.04.2021
comment
Я собрал ваш код в онлайн-компиляторе: onlinegdb.com/HyGr7w0fv_ Возможно, будет полезно добавить его в ваш описание, так как люди могут легко играть с ним ...   -  person 2b-t    schedule 25.04.2021
comment
@ max66 Когда несколько других cpp-файлов включают заголовок и используют шаблоны, эти функции компилируются несколько раз. Дубликаты удаляются компоновщиком. Это приводит к увеличению времени компиляции и компоновки.   -  person JorenHeit    schedule 25.04.2021
comment
@2b-t Спасибо! Добавил вашу ссылку в вопрос :)   -  person JorenHeit    schedule 25.04.2021
comment
Подобный перенос реализаций в файл .tpp не только уменьшает объем кода, который видит пользовательский код, но и ускоряет работу компилятора для них и использует меньше памяти. Другим преимуществом является то, что пользовательский код не зависит от реализации или заголовков, которые включает код реализации, поэтому изменения в реализации не приводят к повторной компиляции пользовательского кода, а только к повторной компоновке. Для обычных заголовков, особенно если они часто редактируются, такая экономия повторных сборок может быть значительной.   -  person Chris Uzdavinis    schedule 25.04.2021
comment
По вопросу технической корректности C++20 [temp.explicit]/ 11 говорится: «Явное создание экземпляра, именующее специализацию шаблона класса, также является явным созданием того же типа (объявление или определение) каждого из его членов (не включая членов, унаследованных от базовых классов, и членов, которые являются шаблонами). ) что... (выделено мной). (Текущий проект упростил формулировку, чтобы просто указать элементы, не являющиеся шаблонами.)   -  person aschepler    schedule 25.04.2021
comment
Так что я думаю, что это также объясняет, почему ответ 2b-t действительно работает.   -  person aschepler    schedule 25.04.2021
comment
@aschepler Объясняет ли это также, почему добавление атрибута (предлагаемого в ответе принимающего) работает? Мне пока не совсем понятно, почему так.   -  person JorenHeit    schedule 25.04.2021
comment
@JorenHeit Нет, __attribute__((used)) вообще не входит в стандарт C ++ и является языковым расширением gcc и clang. Руководство gcc объясняет это: gcc.gnu .org/onlinedocs/gcc-10.3.0/gcc/, не используется.   -  person aschepler    schedule 25.04.2021


Ответы (2)


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

Для нестандартного решения, но работающего на g++ (и, предположительно, clang, но не проверенного), нужно пометить ваши статические элементы данных с помощью используемого атрибута:

template <Enum Value>
struct Instantiate
{
    using Function = int (Type::*)() const;
    static constexpr Function f1 __attribute__((used)) = &Type::get<Value>;
   
    // many more member-functions
};

Обновить

При просмотре стандарта формулировка кажется такой, будто я получил ее с точностью до наоборот:

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

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

person Chris Uzdavinis    schedule 25.04.2021
comment
Спасибо! Это действительно работает. Для меня это все еще странно, потому что как компилятор может доказать, что эти экземпляры не используются в других CU? - person JorenHeit; 25.04.2021
comment
См. мой стандартный комментарий к вопросу. Вкратце, члены базовых классов не создаются. - person aschepler; 25.04.2021

Я пока не могу дать вам хороший ответ, почему это не работает (может быть, я смогу сделать это позже или кто-то другой), но вместо Instantiate и InstantiateAll есть только вариативное InstantiateAll следующим образом < сильно>работает

template <Enum ... Pack>
struct InstantiateAll {
  using Function = int (Type::*)() const;
  static constexpr std::array<Function,sizeof...(Pack)> f = {&Type::get<Pack> ...};
};
template class InstantiateAll<Enum::Member1, Enum::Member2, Enum::Member3>;

Попробуйте здесь.

person 2b-t    schedule 25.04.2021
comment
Спасибо! Это жизнеспособное решение. Я принял ответ Криса, потому что его ответ касается причины, по которой он сейчас не работает. - person JorenHeit; 25.04.2021
comment
Добро пожаловать. Я согласен, что его ответ дает причину, почему. Я немного пробовал, и, например, если бы вы сделали f1 вызовом функции (например, если бы это было static constexpr) вместо указателя на функцию, это тоже сработало бы. Я думаю, что просто наличие указателя, но никто его не использует, заставляет его думать, что он не используется. Неожиданно, но очень интересно... - person 2b-t; 25.04.2021