Fabien
Fabien

Reputation: 13436

How can I make is_pod<T> tests be performed during compilation and not execution?

This might be an easy question, I don't master C++11 templates at all.

I have a generic vector class that is not std::vector<T> for performance reasons (very specific code).

I have observed that checking whether T is a POD or not and, when it is, perform special computations, is much more efficient than not :

void vec<T>::clear() {
  if (!std::is_pod<T>::value) {
    for (int i = 0; i < size; i++) {
       data[i].~T();
    }
  }

  size = 0;
}

Here, I don't call the destructor of T for each item (size can be really huge) and performance is really boosted. But the test if (!std::is_pod<T>::value) is useless once the template was compiled : rather than being compiled to :

void vec<int>::clear() {
  if (false) {
    for (int i = 0; i < size; i++) {
       data[i].~int();
    }
  }

  size = 0;
}

I want it to be compiled to :

void vec<int>::clear() {
  size = 0;
}

Is the compiler "clever" enough to skip if (false) blocks or if (true) tests ? Do I have to write that code somewhat differently ?

Upvotes: 12

Views: 857

Answers (3)

Konrad Rudolph
Konrad Rudolph

Reputation: 545913

Is the compiler "clever" enough to skip if (false) blocks or if (true) tests?

Yes, definitely. Dead code elimination is a trivial optimisation that is performed routinely. Its existence is also crucial to make many debugging libraries work efficiently (= without runtime overhead in release mode).

But I would probably still rewrite this to make it visible to the reader that this is a compile-time distinction, by overloading the function based on is_pod:

void vec<T>::do_clear(std::true_type) { }

void vec<T>::do_clear(std::false_type) {
    for (int i = 0; i < size; i++) {
       data[i].~T();
    }
}

void vec<T>::clear() {
    do_clear(std::is_trivially_destructible<T>());
    size = 0;
}

In the above code I’m using is_trivially_destructible instead of is_pod to make the code more self-explanatory, as suggested by Nicol in the comments. This technique is commonly employed in standard library implementations and other libraries. It’s known as tag dispatching.

Upvotes: 26

Yakk - Adam Nevraumont
Yakk - Adam Nevraumont

Reputation: 275760

Dead code elimination is a common optimization.

However, if you do not trust your compiler to do any optimization at all, you could create a static if template library.

Skip down to the punchline if you don't feel like reading a bunch of pretty horrible hacks.

#include <utility>
#include <type_traits>

template<bool b>
struct static_if_t {
  static_if_t( static_if_t const& ) = default;
  static_if_t() = default;
  static_if_t( static_if_t<b>(*)(std::integral_constant<bool,b>) ) {}
};

template<bool dead>
struct static_if_branch {};

template<bool b>
struct static_else_if_t {
  static_else_if_t( static_else_if_t const& ) = default;
  static_else_if_t() = default;
  static_else_if_t( static_else_if_t<b>(*)(std::integral_constant<bool,b>) ) {}
};

template<bool b>
static_if_t<b> static_if(std::integral_constant<bool,b> unused=std::integral_constant<bool,b>()) {return {};}
template<bool b>
static_else_if_t<b> static_else_if(std::integral_constant<bool,b> unused=std::integral_constant<bool,b>()) {return {};}

static auto static_else = static_else_if<true>;

template<typename Lambda, typename=typename std::enable_if< std::is_same< decltype(std::declval<Lambda&&>()()), decltype(std::declval<Lambda&&>()()) >::value >::type>
static_if_branch<true> operator*( static_if_t<true>, Lambda&& closure )
{
  std::forward<Lambda>(closure)();
  return {};
}
template<typename Lambda, typename=typename std::enable_if< std::is_same< decltype(std::declval<Lambda&&>()()), decltype(std::declval<Lambda&&>()()) >::value >::type>
static_if_branch<false> operator*( static_if_t<false>, Lambda&& /*closure*/ )
{
  return {};
}

template<typename Unused>
static_if_branch<true> operator*( static_if_branch<true>, Unused&& ) {
  return {};
}

static_if_t< true > operator*( static_if_branch<false>, static_else_if_t<true> ) {
  return {};
}
static_if_t< false > operator*( static_if_branch<false>, static_else_if_t<false> ) {
  return {};
}

And here is the punchline:

#include <iostream>

int main() {
  static_if<true>* [&]{
    std::cout << "hello\n";
  } *static_else* [&]{
    std::cout << "doom\n";
  };

  static_if<false>* [&]{
    std::cout << "doom the\n";
  } *static_else* [&]{
    std::cout << "world\n";
  };

  static_if<false>* [&]{
    std::cout << "fello\n";
  } *static_else_if<false>* [&]{
    std::cout << "yellow\n";
  } *static_else_if<false>* [&]{
    std::cout << "hehe\n";
  };

  static_if( std::is_same<int, int>() )* [&]{
    std::cout << "int is int\n";
  };
  static_if( std::is_same<double, double>() )* [&]{
    std::cout << "double is double\n";
  } *static_else_if( std::is_same<int, double>() )* [&]{
    std::cout << "int is double\n";
  } *static_else* [&]{
    std::cout << "sky is not blue\n";
  };
}

but why would you want to do that? Live example

(note that there are two syntaxes the above static_if -- one static_if<compile time boolean expression>, and another static_if( std::is_whatever<blah>() )).

Now, while the above is completely insane, the above technique would let you write a compile time trinary operator that allows a different type based on which branch is picked. Which is neat.

Ie, something like this:

auto result = trinary<std::is_same<A,B>::value>% 7 | 3.14;

and the type of result would be int if A and B are the same type, and double if they differ. Or even:

auto result = meta_trinary<std::is_same<A,B>::value>% [&]{return 7;} | [&]{return 3.14;};

if you prefer, allowing entire blocks of code to be conditionally evaluated, and the conditional type of the return value to be stored.

Upvotes: 2

Andrew Tomazos
Andrew Tomazos

Reputation: 68698

There is a language feature called pseudo destructors which is specifically designed for what you want to do. Basically given a type template parameter T you can syntactically call a destructor for it, and if, when instantiated, T is a scalar type (because for example it is a fundamental type like an int) it will compile and generate a no-op in its place.

For the remainder of POD types that are not scalar, they have trivial destructors, so will likewise generate a no-op.

Any production compiler on even the lowest optimization setting will elide a loop over a no-op. So you can safely write:

void vec<T>::clear() { 
    for (int i = 0; i < size; i++) {
       data[i].~T();
    }

    size = 0;
}

Basically, you are trying to solve an imaginary performance problem the compiler is already taking care of for you.

Upvotes: 10

Related Questions