Pull to refresh

Comments 34

UFO just landed and posted this here

Спасибо за такой подробный и развёрнутый комментарий! Действительно видна погружённость в тему. Про cppinsights знал, но как-то не доводилось много пользоваться при метапрограммировании. Про metashell вообще не знал. Чуть посмотрел. Сходу не понял как пользоваться - но, вероятно, штука крутая.

Что касается продвинутых техник настоящего метапрограммирования - тоже всё верно пишете, это важные темы, я планирую написать о них во второй статье. В данной же хотелось рассказть про шаблоны людям не имевшим с ними дела (и почти не имевшим дела с C++). Уровень целевой аудитории - люди, прошедшие курс C++ (не факт что академический курс, это могли быть онлайн-курсы) и программисты использующие другие языки, которым интересно было бы почитать про плюсы. Отсюда простые примеры, повторения в духе канала Discovery и отсутствие формализма. Я боялся отпугнуть читателей сложными терминами. При таком подходе есть риск ввести в некоторое заблуждение - но, с другой стороны, углубление знаний это часто путь от понимания с упрощениями и неточностями к более глубокому пониманию. Когда людям читают курс по школьной алгебре им говорят что нельзя брать квадратный корень из минус единицы, а потом в университете дают комплексные числа.

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

Камент интереснее статьи и тянет на отдельную статью.

Цель статьи была сделать так чтобы людям стало не так страшно пользоваться шаблонами, а не чтобы блеснуть знаниями. Метпрограммирования, сказать откровенно, боятся и избегают даже в индустрии. Многие коллеги по работе (а я работаю в не самой маленькой фирме) не используют шаблоны потому что они кажутся сложными. Как по мне, такое отношение к метапрограммированию вредит экосистеме C++ и как раз из надежды что-то поменять в этой ситуации была написана статья.

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

P.S.: Коммент @fk01, развёрнутый в полноценную статью, я б тоже с удовольствием почитал.

У вас хорошая статья, спасибо за нее, хотя и базовая. Мне, как человеку, работавшему с С++ много лет назад (и я не использовал шаблоны), очень интересно и понятно.

Я так понимаю, вы планируете цикл статей

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

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

Ну и про проблемы отдельных отладчиков сами же написали.

конкретная реализация шаблона

Спасибо автору за статью и @fk01за отличные дополнения к ней! Я не пишу на языках семейства С (уже лет 20 только фортран! ;-), но краем уха слыхал о шаблонах. Поэтому открыл статью, чтобы понять базовые идеи и принципы. Все зашло без натуги и с первого раза, хотя в моем возрасте изучать что-то новое уже почти невозможно. Особенно ценной мне показалась информация о проблемах отладки. Я ведь использовать язык не планирую, и темой интересуюсь сугубо для расширения кругозора. Поэтому времени на системное изучение вопроса нет и не будет. А при беглом поиске по верхам такую сводку знаний вряд ли найдешь где-то еще!

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

Спасибо. Здорово что базовые идеи стали понятнее!

По поводу вызовов "setElement()" и "getElement()"... Инлайнинг никогда не гарантируется наверняка Чтобы максимально принудить инлайнить вызовы есть всякие специфичные для компиляторов ключевые слова (например, __forceinline).

Тем не менее, в таких тривиальных случаях при оптимизации выше "-O0" с вероятностью 99% компилятор выполнит инлайнинг.

Можно взять какой-нибудь простой код и посмотреть на godbolt.org как будет выполняться оптимизация:

Пример на простом коде

Упрощённый массив:

template<typename Type>
class SimpleArray
{
public:
  	SimpleArray(int inElementsNum)
		: elements(new Type[inElementsNum]), num(inElementsNum)
	{
	}

	Type getElement(int inIndex) const
	{
		return elements[inIndex];
	}

	void setElement(int inIndex, Type inValue)
	{
		elements[inIndex] = inValue;
	}

	~SimpleArray()
	{
		delete[] elements;
  }

private:
	Type* elements = nullptr;
	int num = 0;
};

// -----------------------------------------------------

// Чтобы избежать лишних для нашего теста оптимизаций
// времени компиляции будем задавать количество элементов
// массива из количества элементов командной строки 
int main(int argc, char *argv[])
{
    //-- Int array --

    SimpleArray<int> intArray{ argc };

    for (int i = 0; i < argc; ++i)
        intArray.setElement(i, i);

    int sum1 = 0;
    for (int i = 0; i < argc; ++i)
        sum1 += intArray.getElement(i);

    //-- Raw storage --

    int* rawArray = new int[argc];

    for (int i = 0; i < argc; ++i)
        rawArray[i] = i;

    int sum2 = 0;
    for (int i = 0; i < argc; ++i)
        sum2 += rawArray[i];

	return sum1 + sum2;
}

Уже при минимальной оптимизации "-O1" видно что в ассемблерном коде нет call-вызовов методов:

Ассемблерный код с оптимизацией -O1
main:
        push    r13
        push    r12
        push    rbp
        push    rbx
        sub     rsp, 8
        movsx   r13, edi
        movabs  rax, 2305843009213693950
        cmp     r13, rax
        ja      .L2
        mov     r12d, edi
        sal     r13, 2
        mov     rdi, r13
        call    operator new[](unsigned long)
        mov     rbp, rax
        lea     ecx, [r12-1]
        mov     eax, 0
        test    r12d, r12d
        jle     .L21
.L5:
        mov     DWORD PTR [rbp+0+rax*4], eax
        mov     rdx, rax
        add     rax, 1
        cmp     rdx, rcx
        jne     .L5
        mov     rax, rbp
        lea     rdx, [rbp+4+rcx*4]
        mov     ebx, 0
.L6:
        add     ebx, DWORD PTR [rax]
        add     rax, 4
        cmp     rax, rdx
        jne     .L6
.L3:
        mov     rdi, r13
        call    operator new[](unsigned long)
        jmp     .L22
.L2:
        call    __cxa_throw_bad_array_new_length
.L21:
        mov     ebx, 0
        jmp     .L3
.L22:
        test    r12d, r12d
        jle     .L12
        lea     esi, [r12-1]
        mov     edx, 0
.L8:
        mov     DWORD PTR [rax+rdx*4], edx
        mov     rcx, rdx
        add     rdx, 1
        cmp     rsi, rcx
        jne     .L8
        mov     rdx, rax
        lea     rcx, [rax+4+rsi*4]
        mov     eax, 0
.L9:
        add     eax, DWORD PTR [rdx]
        add     rdx, 4
        cmp     rdx, rcx
        jne     .L9
.L7:
        add     ebx, eax
        mov     rdi, rbp
        call    operator delete[](void*)
        mov     eax, ebx
        add     rsp, 8
        pop     rbx
        pop     rbp
        pop     r12
        pop     r13
        ret
.L12:
        mov     eax, 0
        jmp     .L7
        mov     rbx, rax
        mov     rdi, rbp
        call    operator delete[](void*)
        mov     rdi, rbx
        call    _Unwind_Resume

Для сравнения, вот вариант без оптимизации, в нём есть не заинлайненные функции и их вызовы (например, "call SimpleArray<int>::setElement(int, int)"):

Ассемблерный код с оптимизацией -O0
main:
        push    rbp
        mov     rbp, rsp
        push    rbx
        sub     rsp, 72
        mov     DWORD PTR [rbp-68], edi
        mov     QWORD PTR [rbp-80], rsi
        mov     edx, DWORD PTR [rbp-68]
        lea     rax, [rbp-64]
        mov     esi, edx
        mov     rdi, rax
        call    SimpleArray<int>::SimpleArray(int) [complete object constructor]
        mov     DWORD PTR [rbp-20], 0
.L3:
        mov     eax, DWORD PTR [rbp-20]
        cmp     eax, DWORD PTR [rbp-68]
        jge     .L2
        mov     edx, DWORD PTR [rbp-20]
        mov     ecx, DWORD PTR [rbp-20]
        lea     rax, [rbp-64]
        mov     esi, ecx
        mov     rdi, rax
        call    SimpleArray<int>::setElement(int, int)
        add     DWORD PTR [rbp-20], 1
        jmp     .L3
.L2:
        mov     DWORD PTR [rbp-24], 0
        mov     DWORD PTR [rbp-28], 0
.L5:
        mov     eax, DWORD PTR [rbp-28]
        cmp     eax, DWORD PTR [rbp-68]
        jge     .L4
        mov     edx, DWORD PTR [rbp-28]
        lea     rax, [rbp-64]
        mov     esi, edx
        mov     rdi, rax
        call    SimpleArray<int>::getElement(int) const
        add     DWORD PTR [rbp-24], eax
        add     DWORD PTR [rbp-28], 1
        jmp     .L5
.L4:
        mov     eax, DWORD PTR [rbp-68]
        cdqe
        movabs  rdx, 2305843009213693950
        cmp     rax, rdx
        ja      .L6
        sal     rax, 2
        mov     rdi, rax
        call    operator new[](unsigned long)
        jmp     .L15
.L6:
        call    __cxa_throw_bad_array_new_length
.L15:
        mov     QWORD PTR [rbp-48], rax
        mov     DWORD PTR [rbp-32], 0
.L9:
        mov     eax, DWORD PTR [rbp-32]
        cmp     eax, DWORD PTR [rbp-68]
        jge     .L8
        mov     eax, DWORD PTR [rbp-32]
        cdqe
        lea     rdx, [0+rax*4]
        mov     rax, QWORD PTR [rbp-48]
        add     rdx, rax
        mov     eax, DWORD PTR [rbp-32]
        mov     DWORD PTR [rdx], eax
        add     DWORD PTR [rbp-32], 1
        jmp     .L9
.L8:
        mov     DWORD PTR [rbp-36], 0
        mov     DWORD PTR [rbp-40], 0
.L11:
        mov     eax, DWORD PTR [rbp-40]
        cmp     eax, DWORD PTR [rbp-68]
        jge     .L10
        mov     eax, DWORD PTR [rbp-40]
        cdqe
        lea     rdx, [0+rax*4]
        mov     rax, QWORD PTR [rbp-48]
        add     rax, rdx
        mov     eax, DWORD PTR [rax]
        add     DWORD PTR [rbp-36], eax
        add     DWORD PTR [rbp-40], 1
        jmp     .L11
.L10:
        mov     edx, DWORD PTR [rbp-24]
        mov     eax, DWORD PTR [rbp-36]
        lea     ebx, [rdx+rax]
        lea     rax, [rbp-64]
        mov     rdi, rax
        call    SimpleArray<int>::~SimpleArray() [complete object destructor]
        mov     eax, ebx
        jmp     .L16
        mov     rbx, rax
        lea     rax, [rbp-64]
        mov     rdi, rax
        call    SimpleArray<int>::~SimpleArray() [complete object destructor]
        mov     rax, rbx
        mov     rdi, rax
        call    _Unwind_Resume
.L16:
        add     rsp, 72
        pop     rbx
        pop     rbp
        ret
SimpleArray<int>::SimpleArray(int) [base object constructor]:
        push    rbp
        mov     rbp, rsp
        sub     rsp, 16
        mov     QWORD PTR [rbp-8], rdi
        mov     DWORD PTR [rbp-12], esi
        mov     eax, DWORD PTR [rbp-12]
        cdqe
        movabs  rdx, 2305843009213693950
        cmp     rax, rdx
        ja      .L18
        sal     rax, 2
        jmp     .L20
.L18:
        call    __cxa_throw_bad_array_new_length
.L20:
        mov     rdi, rax
        call    operator new[](unsigned long)
        mov     rdx, rax
        mov     rax, QWORD PTR [rbp-8]
        mov     QWORD PTR [rax], rdx
        mov     rax, QWORD PTR [rbp-8]
        mov     edx, DWORD PTR [rbp-12]
        mov     DWORD PTR [rax+8], edx
        nop
        leave
        ret
SimpleArray<int>::~SimpleArray() [base object destructor]:
        push    rbp
        mov     rbp, rsp
        sub     rsp, 16
        mov     QWORD PTR [rbp-8], rdi
        mov     rax, QWORD PTR [rbp-8]
        mov     rax, QWORD PTR [rax]
        test    rax, rax
        je      .L23
        mov     rax, QWORD PTR [rbp-8]
        mov     rax, QWORD PTR [rax]
        mov     rdi, rax
        call    operator delete[](void*)
.L23:
        nop
        leave
        ret
SimpleArray<int>::setElement(int, int):
        push    rbp
        mov     rbp, rsp
        mov     QWORD PTR [rbp-8], rdi
        mov     DWORD PTR [rbp-12], esi
        mov     DWORD PTR [rbp-16], edx
        mov     rax, QWORD PTR [rbp-8]
        mov     rax, QWORD PTR [rax]
        mov     edx, DWORD PTR [rbp-12]
        movsx   rdx, edx
        sal     rdx, 2
        add     rdx, rax
        mov     eax, DWORD PTR [rbp-16]
        mov     DWORD PTR [rdx], eax
        nop
        pop     rbp
        ret
SimpleArray<int>::getElement(int) const:
        push    rbp
        mov     rbp, rsp
        mov     QWORD PTR [rbp-8], rdi
        mov     DWORD PTR [rbp-12], esi
        mov     rax, QWORD PTR [rbp-8]
        mov     rax, QWORD PTR [rax]
        mov     edx, DWORD PTR [rbp-12]
        movsx   rdx, edx
        sal     rdx, 2
        add     rax, rdx
        mov     eax, DWORD PTR [rax]
        pop     rbp
        ret

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

А если я пишу вот так:

auto max(auto a, auto b) {
    return (a >= b ? a : b);
}

Это шаблоны или что? И, если это не шаблоны, то чем отличается от:

template<typename T>
T max(T a, T b) {
	return (a >= b ? a : b);
}
Это шаблоны или что?
Начиная с C++20 это сокращение для шаблона (ничем не отличается от варианта ниже, разве что T может быть свой для каждого аргумента и возвращаемого значения). В предыдущих версиях это невалидный код.

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

Всё типы всё равно будут определены на компиляции, а значит это не будет также как на питоне. Ну и конечно возвращать нужно не auto а decltype(auto), ведь может оказаться и ссылка в общем случае.

Ну и разница большая, версия с auto эквивалентна
template<typename T, typename U>
auto max(T a, U b);

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

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

To be continued... Я работаю над второй частью)

Статья огонь.
1) Можно было бы упомянуть о шаблонных шаблонных параметрах — где параметром шаблона является шаблон. В 1-м комментарии об этом упоминается. Возможно это будет в других статьях.
2) Для меня, как не сильного спеца по плюсам впечатлила книга Андрея Александреску «Современное проектирование на C++» — первая часть книги для меня была магической.

Да, Александреску топ! С приходом концептов, вероятно, многие решения из неё ощутимо человекочитаемее станут.

В стандартной библиотеке шаблонов эту структуру данных реализует шаблон класса "std::vector<>".

Скорее std::array

Ну, на тот момент ещё std::vector, так как размер там в рантайме передаётся. Про std::array в разделе про шаблонные аргументы-константы написано, где размер определяется в компайлтайм.

П.С.: В целом, спасибо что обратили внимание. Я с ремаркой про C++20 о std::span там напишу ещё, он ближе всего по смыслу.

Точнее, не ближе всего - но тоже близок

const int max = max<int>(abMax, c);

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

Справедливо, спасибо большое что заметили. Поправлю

Я, кстати, не понял почему утащило

Я не в курсе чем провинился этот пользователь, но видимо если аккаунт был деактивирован/удалён, то все комменты тоже исчезают.
По ссылке — не помогло :( Скопипасти коммент, если не сложно (с указанием авторства естественно)
fk01 10.01.2022 в 05:07

Я бы сказал так, что шаблоны — это на самом деле совсем не шаблоны… На самом деле C++ — это несколько отдельных, по меньшей мере три, языка программирования:

1. язык C-препроцессора осуществляющий подстановку на уровне текста — работает даже не во время, а до компиляции;

2. усовершенствованная версия языка C с классами;

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

Результатом работы упомянутого декларативного языка является генерация некой условной, в явном виде не отображаемой программисту, программы для C-с-классами (подробности можно подглядеть на https://cppinsights.io/).

Основными функциями этого воображаемого декларативного языка, как части C++, являются:

  • преобразование типов (неявное, с использованием пользовательских функций преобразования типов, подстановка конструкторов);
  • статическая диспетчеризация, function lookup, зависимая от типов аргументов, поддержка концепции SFINAE;
  • и самое главное, собственно вишенка на торте: шаблоны.

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

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

Важно понимать, что подразумевается под понятием типа. Тип — это не описание того, как данные хранятся в памяти (структура), это пока не важно (это важно будет для компилятора C с классами). Важно что тип может обладать некоторым набором свойств. В частности вложенными типами, вложенными правилами вывода типов и т.д. и т.п. И на самом деле в пространстве типов попросту возможно программирование, вся программа может целиком исполняться в процессе компиляции, а не после компиляции. Конечно программирование в таком виде не слишком удобно и не предназначено для решения прикладных задач, а предназначено для задач метапрограммирования.

На самом деле конечно описанный декларативный язык не совсем декларативный. Некоторые вещи, например Argument Dependent Lookup начинает зависеть от порядка и видимости деклараций, раскрытие шаблонов тоже. В частности существует такая штука как type loophole которая попросту паразитирует на том факте, что порядок деклараций в C++ всё же имеет значение…

Возвращаясь к шаблонам. Шаблоны названы так исторически. Технически шаблон это совершенно не подстановка текста. Подстановка текста — это C-препроцессор, из чего изначально шаблоны родились. Шаблоны — это именно набор деклараций, используя которые компилятор самостоятельно ищет решение. И в вашей статье описывается, мол есть шаблоны классов, шаблоны функций, шаблоны переменных наконец (начиная с C++14). Но это всё на самом деле не слишком принципиально. Это скорей историческое наследие: типичный пример использования шаблона функций сейчас в метапрограммировании — это отнюдь не генерация функции, а использование автоматического вывода типов. Даже без тела собственно самой функции. Чтоб в шаблоне функции попросту получить типы в виде параметров шаблона (их компилятор автоматически выведет) и использовать их где-то дальше, например для параметризации уже другого шаблона класса. Да, шаблоны можно использовать и для генерации классов, и для генерации функций и переменных, но это давно не единственное их предназначение. Основным я бы назвал реализацию приёмов метапрограммирования.

Процитирую некоторые моменты:
В терминах C++ обобщённое описание функции называется шаблоном функции.

Да и нет. В силу возможности специализации для конкретных параметров шаблона, важным отличием шаблонов в C++ от дженериков в C# и других языках является то, что собственно сами специализации могут быть кардинально разными в зависимости от параметров. Т.е. это не просто подстановка типа, Функция просто может делать совсем что-то другое. Или, в случае специализации класса, специализация для конкретного типа, например, может породить что-то совершенно отличное от обощённой специализации. Данная практика широко используется для задач метапрограмирования.

Так же стоит сказать о порождении класса из шаблона. Принципиальным является то, что класс из шаблона появляется в момент его первого использования (фактически, это в каком-то смысле ленивое вычисление в пространстве типов). И потом уже существует до конца работы компилятора. При определённых обстоятельствах факт генерирования нового типа из шаблона («инстанциации» шаблона) может иметь побочные эффекты и их можно обнаружить. И самое важное, типы от которых шаблон зависит тоже будут востребованы в момент генерации типа из шаблона, а не в момент парсинга шаблона компилятором. В момент парсинга компилятор вынужден работать с определениями которые ещё не определены (отсюда и нужда руками выписывать ключевые слова typename и template внутри шаблона, отсюда же и необходимость создания алиасов для типов, которые до генерации типа из шаблона не видны в пространстве имён, например в случае наследования от класса заданного в параметрах шаблона).

Нужно заметить, что концепты сами по себе не является каким-то новым свойством, по сути это такой синтакс-сахар для удобной замены std::enable_if. Ключевым моментом здесь является не концепт, а сама концепция SFINAE реализуемая C++-компилятором и не реализуемая во многих других языках. Как было сказано выше, компилятор ищет решение, возможную подстановку, и может при этом откинуть некорректные варианты без ошибки.

Не рассмотрен такой важный вопрос, что в C++ параметром шаблона может являться другой шаблон. Это тоже принципиальное отличие C++ от других языков с обобщёнными функциями (generics). Шаблон принявший другой шаблон сам может параметризовать его как ему нужно и начать использовать полученные из этого шаблона функции и типы. Без такого функционала, метапрограммирование в C++ было бы очень ограничено.

Так же опущен вопрос переменного числа аргументов в параметрах шаблона. Это — важный функцинал, он позволяет рекурсиивные вычисления в простанстве типов. И в частности можно обрабатывать списки, что часто практически очень удобно.

Процитирую ещё:
Макросы выполняют текстовую подстановку аргументов, в то время как шаблоны лексически и синтаксически проверяются компилятором

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

Затрудняют ли шаблоны отладку кода?

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

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

1. ошибка компиляции — для gcc или clang сейчас вывод компилятора достаточно хорош, показывается весь путь раскрытия шаблонов, параметры шаблонов. Ситуация реально лучше, чем было лет 10 тому назад. Компиляторы не полностью разворачивают всю историю, и если не хватает вывода, то можно использовать опцию -ftemplate-backtrace-limit.

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

PS: в статье на мой взгляд слишком длинные и нудные примеры, что затрудняет понимание новичками. Примеры по-моему лучше сделать короткими, и заодно помимо исторических использований шаблонов показать основные ключевые приёмы метапрограммирования. Например такие как std::void_t, std::integer_sequence, std::enable_if и ключевое слово decltype (чего так же нет во многих других других языках, а в C существует в виде нестандартного typeof), std::declval (который в голом C спрятан в макросе offsetof), можно показать, как шаблоны могут использоваться для вывода возвращаемого значения и типов аргументов другой функции и тому подобные вещи…

Статья классная, спасибо. Уже начал ждать вторую часть ;)

Спасибо за статью, было интересно читать - особенно код примеров. Жду продолжение

Увы, в связи войной которую начала здесь Россия продолжение будет очень нескоро

Sign up to leave a comment.

Articles