Что делают компиляторы с разветвлением во время компиляции?
EDIT: В качестве примера я использовал случай if if else, который иногда может быть разрешен во время компиляции (например, когда задействованы статические значения, cf ). Адаптация ответов ниже для других типов статического ветвления (например, нескольких ветвей или ветвей с несколькими критериями) должна быть простой. Обратите внимание, что разветвление во время компиляции с использованием шаблона-мета-программирования не является темой здесь.
В типичном коде, подобном этому
#include template T numeric_procedure( const T& x ) { if ( std::is_integral::value ) { // Integral types } else { // Floating point numeric types } }
будет ли компилятор оптимизировать выражение if / else, когда я буду определять конкретные типы шаблонов позже в моем коде?
- Что такое руководства по вычитанию шаблонов и когда мы должны их использовать?
- Может ли шаблон функции члена classа C ++ быть виртуальным?
- Магические аргументы в шаблонах функций
- SFINAE работает в обратном типе, но не как параметр шаблона
- перегрузка оператора друга << для шаблона classа
Простой альтернативой было бы написать что-то вроде этого:
#include template inline T numeric_procedure( const T& x ) { return numeric_procedure_impl( x, std::is_integral() ); } // ------------------------------------------------------------------------ template T numeric_procedure_impl( const T& x, std::true_type const ) { // Integral types } template T numeric_procedure_impl( const T& x, std::false_type const ) { // Floating point numeric types }
Есть ли разница в производительности между этими решениями? Есть ли какие-либо субъективные основания говорить, что один лучше другого? Существуют ли другие (возможно, лучшие) решения для борьбы с разветвлением во время компиляции?
- Расшифровка сообщений об ошибках шаблона C ++
- Специализированная специализированная специализация classа, в которой шаблонный шаблон является шаблоном
- Как получить список текущих переменных из шаблона Jinja 2?
- Как вызвать функцию члена шаблона?
- В чем отличия между Mustache.js и Handlebars.js?
- Можете ли вы рекомендовать механизм шаблонов .net?
- Как определить существование classа с использованием SFINAE?
- C ++ Template Metaprogramming - Можно ли выводить сгенерированный код?
TL; DR
Существует несколько способов получить различное поведение во время выполнения, зависящее от параметра шаблона. Производительность не должна быть вашей главной задачей здесь, но гибкость и ремонтопригодность должны быть. Во всех случаях различные тонкие обертки и постоянные условные выражения будут оптимизированы на любом достойном компиляторе для создания релизов. Ниже небольшое резюме с различными компромиссами (вдохновлено этим ответом @AndyProwl).
Время выполнения, если
Первое решение – это простое время выполнения, if
:
template T numeric_procedure(const T& x) { if (std::is_integral::value) { // valid code for integral types } else { // valid code for non-integral types, // must ALSO compile for integral types } }
Он прост и эффективен: любой достойный компилятор оптимизирует мертвую ветку.
Существует несколько недостатков:
- на некоторых платформах (MSVC) постоянное условное выражение дает ложное предупреждение о компиляторе, которое затем необходимо игнорировать или отключать.
- Но, что еще хуже, на всех соответствующих платформах обе ветви оператора
if/else
должны действительно компилироваться для всех типовT
, даже если одна из ветвей, как известно, не должна быть взята. ЕслиT
содержит разные типы членов в зависимости от его характера, то вы получите ошибку компилятора, как только вы попытаетесь получить к ним доступ.
Отправка тегов
Ваш второй подход известен как диспетчеризация тегов:
template T numeric_procedure_impl(const T& x, std::false_type) { // valid code for non-integral types, // CAN contain code that is invalid for integral types } template T numeric_procedure_impl(const T& x, std::true_type) { // valid code for integral types } template T numeric_procedure(const T& x) { return numeric_procedure_impl(x, std::is_integral()); }
Он работает нормально, без накладных расходов: временный std::is_integral
и вызов однострочной вспомогательной функции будет оптимизирован на любой приемлемой платформе.
Основной (незначительный недостаток IMO) заключается в том, что у вас есть шаблон с 3 вместо 1 функции.
SFINAE
Тесно связанная с отправкой тегов – SFINAE (сбой замены не является ошибкой)
template::value>::type> T numeric_procedure(const T& x) { // valid code for non-integral types, // CAN contain code that is invalid for integral types } template ::value>::type> T numeric_procedure(const T& x) { // valid code for integral types }
Это имеет тот же эффект, что и диспетчеризация тегов, но работает несколько иначе. Вместо того, чтобы использовать аргумент-вывод для выбора надлежащей вспомогательной перегрузки, он непосредственно манипулирует набором перегрузки для вашей основной функции.
Недостаток заключается в том, что он может быть хрупким и сложным способом, если вы точно не знаете, что такое весь набор перегрузки (например, с тяжелым кодом шаблона, ADL может вывести больше перегрузок из связанных пространств имен, о которых вы не думали ). И по сравнению с диспетчеризацией меток выбор, основанный на чем-либо, кроме бинарного решения, намного более активен.
Частичная специализация
Другой подход заключается в использовании вспомогательного элемента шаблона classа с оператором приложения приложения и частично его специализации
template struct numeric_functor; template struct numeric_functor { T operator()(T const& x) const { // valid code for non-integral types, // CAN contain code that is invalid for integral types } }; template struct numeric_functor { T operator()(T const& x) const { // valid code for integral types } }; template T numeric_procedure(T const& x) { return numeric_functor::value>()(x); }
Это, вероятно, самый гибкий подход, если вы хотите иметь мелкомасштабный контроль и минимальное дублирование кода (например, если вы также хотите специализироваться на размере и / или выравнивании, но говорите только для типов с плавающей точкой). Соответствие шаблонов, заданное с помощью частичной специализированности шаблонов, идеально подходит для таких сложных задач. Как и для диспетчеризации меток, вспомогательные функции оптимизируются любым достойным компилятором.
Основным недостатком является немного большая котельная плита, если вы хотите специализироваться только на одном двоичном условии.
Если constexpr (предложение C ++ 1z)
Это перезагрузка неудачных ранее предложений для static if
(которые используются на языке программирования D)
template T numeric_procedure(const T& x) { if constexpr (std::is_integral::value) { // valid code for integral types } else { // valid code for non-integral types, // CAN contain code that is invalid for integral types } }
Как и во время выполнения, if
все в одном месте, но главным преимуществом здесь является то, else
ветвь else
будет полностью отброшена компилятором, когда, как известно, ее не следует принимать. Большим преимуществом является то, что вы сохраняете весь код локальным и не должны использовать небольшие вспомогательные функции, такие как диспетчеризация меток или частичная специализация шаблонов.
Concepts-Lite (предложение C ++ 1z)
Concepts-Lite – это предстоящая техническая спецификация, которая должна стать частью следующей крупной версии C ++ (C ++ 1z, с z==7
как наилучшее предположение).
template T numeric_procedure(const T& x) { // valid code for non-integral types, // CAN contain code that is invalid for integral types } template T numeric_procedure(const T& x) { // valid code for integral types }
Этот подход заменяет ключевое слово class
или typename
внутри template< >
скобками названием концепции, описывающим семейство типов, для которых должен работать код. Это можно рассматривать как обобщение методов диспетчеризации меток и SFINAE. Некоторые компиляторы (gcc, Clang) имеют экспериментальную поддержку этой функции. Прилагательное Lite ссылается на неудачное предложение Concepts C ++ 11.
Обратите внимание, что хотя оптимизатор вполне может обрезать статически известные тесты и недостижимые ветки из сгенерированного кода, компилятор все равно должен иметь возможность компилировать каждую ветвь.
То есть:
int foo() { #if 0 return std::cout << "this isn't going to work\n"; #else return 1; #endif }
будет работать нормально, потому что препроцессор удаляет мертвую ветвь, прежде чем компилятор увидит ее, но:
int foo() { if (std::is_integral::value) { return std::cout << "this isn't going to work\n"; } else { return 1; } }
не будет. Несмотря на то, что оптимизатор может отбросить первую ветвь, она все равно не скомпилируется. Здесь используется справка enable_if
и SFINAE, так как вы можете выбрать допустимый (компилируемый) код и недействительный (несовместимый) код «Сбой компиляции не является ошибкой».
Компилятор может быть достаточно умным, чтобы увидеть, что он может заменить тело оператора С if
двумя различными реализациями функций и просто выбрать правильный. Но с 2014 года я сомневаюсь, что есть какой-то компилятор, который достаточно умен, чтобы сделать это. Возможно, я ошибаюсь. std::is_integral
, std::is_integral
достаточно прост, и я думаю, что он будет оптимизирован.
Ваша идея перегрузки по результату std::is_integral
– одно из возможных решений.
Другое и более эффективное решение IMHO – использовать std::enable_if
(вместе с std::is_integral
).
Кредит @MooingDuck и @Casey
template decltype(auto) if_else_impl(std::true_type, FN1 &&fn1, FN2 &&, Args&&... args) { return fn1(std::forward(args)...); } template decltype(auto) if_else_impl(std::false_type, FN1 &&, FN2 &&fn2, Args&&... args) { return fn2(std::forward(args)...); } #define static_if(...) if_else_impl(__VA_ARGS__, *this)
И обычай прост как:
static_if(do_it, [&](auto& self){ return 1; }, [&](auto& self){ return self.sum(2); } );
Работает как статический, если – компилятор переходит только в «истинную» ветвь.
PS Вам нужно иметь self = *this
и делать из него вызовы членов, из-за ошибки gcc . Если у вас есть вложенные lambda-вызовы, вы не можете использовать this->
вместо self.