c++ - What do compilers do with compile-time branching? -
edit: took "if/else" case example can resolved @ compile time (eg when static values involved, cf <type_traits>
). adapting answers below other types of static branching (eg, multiple branches or multi-criteria branches) should straightforward. note compile-time branching using template-meta programming not topic here.
in typical code this
#include <type_traits> template <class t> t numeric_procedure( const t& x ) { if ( std::is_integral<t>::value ) { // integral types } else { // floating point numeric types } }
will compiler optimize if/else statement out when define specific template types later on in code?
a simple alternative write this:
#include <type_traits> template <class t> inline t numeric_procedure( const t& x ) { return numeric_procedure_impl( x, std::is_integral<t>() ); } // ------------------------------------------------------------------------ template <class t> t numeric_procedure_impl( const t& x, std::true_type const ) { // integral types } template <class t> t numeric_procedure_impl( const t& x, std::false_type const ) { // floating point numeric types }
is there difference in terms of performance between these solutions? there non-subjective grounds saying 1 better other? there other (possibly better) solutions deal compile-time branching?
tl;dr
there several ways different run-time behavior dependent on template parameter. performance should not primary concern here, flexibility , maintainability should. in cases, various thin wrappers , constant conditional expressions optimized away on decent compiler release builds. below small summary various tradeoffs (inspired this answer @andyprowl).
run-time if
your first solution simple run-time if
:
template<class t> t numeric_procedure(const t& x) { if (std::is_integral<t>::value) { // valid code integral types } else { // valid code non-integral types, // must compile integral types } }
it simple , effective: decent compiler optimize away dead branch.
there several disadvantages:
- on platforms (msvc), constant conditional expression yields spurious compiler warning need ignore or silence.
- but worse, on conforming platforms, both branches of
if/else
statement need compile typest
, if 1 of branches known not taken. ift
contains different member types depending on nature, compiler error try access them.
tag dispatching
your second approach known tag-dispatching:
template<class t> t numeric_procedure_impl(const t& x, std::false_type) { // valid code non-integral types, // can contain code invalid integral types } template<class t> t numeric_procedure_impl(const t& x, std::true_type) { // valid code integral types } template<class t> t numeric_procedure(const t& x) { return numeric_procedure_impl(x, std::is_integral<t>()); }
it works fine, without run-time overhead: temporary std::is_integral<t>()
, call one-line helper function both optimized way on decent platform.
the main (minor imo) disadvantage have boilerplate 3 instead of 1 function.
sfinae
closely related tag-dispatching sfinae (substitution failure not error)
template<class t, class = typename std::enable_if<!std::is_integral<t>::value>::type> t numeric_procedure(const t& x) { // valid code non-integral types, // can contain code invalid integral types } template<class t, class = typename std::enable_if<std::is_integral<t>::value>::type> t numeric_procedure(const t& x) { // valid code integral types }
this has same effect tag-dispatching works differently. instead of using argument-deduction select proper helper overload, directly manipulates overload set main function.
the disadvantage can fragile , tricky way if don't know entire overload set (e.g. template heavy code, adl pull in more overloads associated namespaces didn't think of). , compared tag-dispatching, selection based on other binary decision lot more involved.
partial specialization
another approach use class template helper function application operator , partially specialize it
template<class t, bool> struct numeric_functor; template<class t> struct numeric_functor<t, false> { t operator()(t const& x) const { // valid code non-integral types, // can contain code invalid integral types } }; template<class t> struct numeric_functor<t, true> { t operator()(t const& x) const { // valid code integral types } }; template<class t> t numeric_procedure(t const& x) { return numeric_functor<t, std::is_integral<t>::value>()(x); }
this flexible approach if want have fine-grained control , minimal code duplication (e.g. if want specialize on size and/or alignment, floating point types). pattern matching given partial template specialization ideally suited such advanced problems. tag-dispatching, helper functors optimized away decent compiler.
the main disadvantage larger boiler-plate if want specialize on single binary condition.
if constexpr (c++1z proposal)
this reboot of failed earlier proposals static if
(which used in d programming language)
template<class t> t numeric_procedure(const t& x) { if constexpr (std::is_integral<t>::value) { // valid code integral types } else { // valid code non-integral types, // can contain code invalid integral types } }
as run-time if
, in 1 place, main advantage here else
branch dropped entirely compiler when known not taken. great advantage keep code local, , not have use little helper functions in tag dispatching or partial template specialization.
concepts-lite (c++1z proposal)
concepts-lite upcoming technical specification scheduled part of next major c++ release (c++1z, z==7
best guess).
template<non_integral t> t numeric_procedure(const t& x) { // valid code non-integral types, // can contain code invalid integral types } template<integral t> t numeric_procedure(const t& x) { // valid code integral types }
this approach replaces class
or typename
keyword inside template< >
brackets concept name describing family of types code supposed work for. can seen generalization of tag-dispatching , sfinae techniques. compilers (gcc, clang) have experimental support feature. lite adjective referring failed concepts c++11 proposal.
Comments
Post a Comment