Как я могу сделать тесты is_pod <T> во время компиляции, а не выполнения?

Это может быть простой вопрос, я вообще не разбираюсь в шаблонах С++ 11.

У меня есть общий векторный класс, который не является std::vector<T> по соображениям производительности (очень конкретный код).

Я заметил, что проверка того, является ли T POD или нет, и, когда это так, выполняет специальные вычисления, намного эффективнее, чем нет:

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

  size = 0;
}

Здесь я не называю деструктор T для каждого элемента (size может быть действительно огромным), и производительность действительно повышается. Но тест if (!std::is_pod<T>::value) бесполезен после компиляции шаблона: вместо компиляции:

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

  size = 0;
}

Я хочу, чтобы он был скомпилирован для:

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

Является ли компилятор "умным" достаточно, чтобы пропустить теги if (false) или if (true)? Должен ли я писать этот код несколько иначе?

Ответ 1

Является ли компилятор "умным" достаточно, чтобы пропустить, если (false) блокирует или если (true) тесты?

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

Но я, вероятно, все же переписал бы это, чтобы читателю было ясно, что это различие во времени компиляции, перегружая функцию на основе 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;
}

В приведенном выше коде Im использует is_trivially_destructible вместо is_pod, чтобы сделать код более понятным, как было предложено Nicol в комментарии. Этот метод обычно используется в стандартных реализациях библиотек и других библиотеках. Его называют отправкой тега.

Ответ 2

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

Для оставшихся типов POD, которые не являются скалярными, у них есть тривиальные деструкторы, поэтому он также будет генерировать no-op.

Любой производственный компилятор даже при самом низком параметре оптимизации выйдет из цикла без операции. Поэтому вы можете спокойно писать:

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

    size = 0;
}

В принципе, вы пытаетесь решить мнимую проблему производительности, которую компилятор уже заботится о вас.

Ответ 3

Исключение мертвого кода - это общая оптимизация.

Однако, если вы не доверяете своему компилятору вообще никакой оптимизации, вы можете создать статическую if template library.

Проскользните на пуанлинг, если вам не хочется читать кучу довольно ужасных хаков.

#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 {};
}

И вот пуанлинг:

#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";
  };
}

но почему вы хотите это сделать? Живой пример

(обратите внимание, что существует два синтаксиса выше static_if - один static_if<compile time boolean expression>, а другой static_if( std::is_whatever<blah>() )).

Теперь, в то время как вышесказанное совершенно безумно, вышеупомянутый метод позволит вам написать оператор trile time времени компиляции, который позволяет использовать другой тип, основанный на том, какая ветка выбрана. Что аккуратно.

То есть, что-то вроде этого:

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

и тип result будет int, если A и B являются одним и тем же типом, а double, если они отличаются. Или даже:

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

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